mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into replace_toasts
This commit is contained in:
		
						commit
						759ed34cf2
					
				
					 922 changed files with 29317 additions and 44373 deletions
				
			
		
							
								
								
									
										18
									
								
								.github/workflows/android.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/android.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| name: Android CI | ||||
| 
 | ||||
| on: [push, pull_request] | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| concurrency: | ||||
|   group: build-${{ github.event.pull_request.number || github.ref }} | ||||
|  | @ -12,17 +12,17 @@ jobs: | |||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v2.4.0 | ||||
|         uses: actions/checkout@v3 | ||||
| 
 | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@v2.5.0 | ||||
|         uses: actions/setup-java@v3 | ||||
|         with: | ||||
|           distribution: "temurin" | ||||
|           java-version: 11 | ||||
|           distribution: 'temurin' | ||||
|           java-version: '17' | ||||
| 
 | ||||
|       - name: Cache packages | ||||
|         id: cache-packages | ||||
|         uses: actions/cache@v2.1.7 | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: | | ||||
|             ~/.gradle/caches | ||||
|  | @ -37,7 +37,7 @@ jobs: | |||
| 
 | ||||
|       - name: AVD cache | ||||
|         if: github.event_name != 'pull_request' | ||||
|         uses: actions/cache@v2 | ||||
|         uses: actions/cache@v3 | ||||
|         id: avd-cache | ||||
|         with: | ||||
|           path: | | ||||
|  | @ -89,7 +89,7 @@ jobs: | |||
|         run: bash ./gradlew assembleBetaDebug --stacktrace | ||||
| 
 | ||||
|       - name: Upload betaDebug APK | ||||
|         uses: actions/upload-artifact@v2.3.1 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: betaDebugAPK | ||||
|           path: app/build/outputs/apk/beta/debug/app-*.apk | ||||
|  | @ -98,7 +98,7 @@ jobs: | |||
|         run: bash ./gradlew assembleProdDebug --stacktrace | ||||
| 
 | ||||
|       - name: Upload prodDebug APK | ||||
|         uses: actions/upload-artifact@v2.3.1 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: prodDebugAPK | ||||
|           path: app/build/outputs/apk/prod/debug/app-*.apk | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -43,3 +43,7 @@ app/src/main/jniLibs | |||
| #https://docs.opencv.org/3.3.0/ | ||||
| /libraries/opencv/javadoc/ | ||||
| captures/* | ||||
| 
 | ||||
| # Test and other output | ||||
| app/jacoco.exec | ||||
| app/CommonsContributions | ||||
							
								
								
									
										17
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							|  | @ -39,21 +39,18 @@ | |||
|       <option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" /> | ||||
|       <option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" /> | ||||
|     </Objective-C> | ||||
|     <Objective-C-extensions> | ||||
|       <extensions> | ||||
|         <pair source="cc" header="h" fileNamingConvention="NONE" /> | ||||
|         <pair source="c" header="h" fileNamingConvention="NONE" /> | ||||
|       </extensions> | ||||
|     </Objective-C-extensions> | ||||
|     <Python> | ||||
|       <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> | ||||
|     </Python> | ||||
|     <TypeScriptCodeStyleSettings> | ||||
|       <option name="INDENT_CHAINED_CALLS" value="false" /> | ||||
|     </TypeScriptCodeStyleSettings> | ||||
|     <XML> | ||||
|       <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> | ||||
|     </XML> | ||||
|     <files> | ||||
|       <extensions> | ||||
|         <pair source="cc" header="h" fileNamingConvention="NONE" /> | ||||
|         <pair source="c" header="h" fileNamingConvention="NONE" /> | ||||
|       </extensions> | ||||
|     </files> | ||||
|     <codeStyleSettings language="CSS"> | ||||
|       <indentOptions> | ||||
|         <option name="INDENT_SIZE" value="2" /> | ||||
|  | @ -318,9 +315,7 @@ | |||
|     <codeStyleSettings language="protobuf"> | ||||
|       <option name="RIGHT_MARGIN" value="80" /> | ||||
|       <indentOptions> | ||||
|         <option name="INDENT_SIZE" value="2" /> | ||||
|         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||
|         <option name="TAB_SIZE" value="2" /> | ||||
|       </indentOptions> | ||||
|     </codeStyleSettings> | ||||
|   </code_scheme> | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.mailmap
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.mailmap
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| # See: https://git-scm.com/docs/git-shortlog#_mapping_authors | ||||
| # | ||||
| Brooke Vibber <bvibber@wikimedia.org> | ||||
| Brooke Vibber <bvibber@wikimedia.org> <brion@wikimedia.org> | ||||
| Brooke Vibber <bvibber@wikimedia.org> <brion@pobox.com> | ||||
							
								
								
									
										149
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										149
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -1,5 +1,154 @@ | |||
| # Wikimedia Commons for Android | ||||
| 
 | ||||
| ## v5.0.2 | ||||
| 
 | ||||
| - Enhanced multi-upload functionality with user prompts to clarify that all images would share the | ||||
|   same category and depictions. | ||||
| - Show Wikidata description on currently active Nearby pin to provide more useful information. | ||||
| - Improve the visibility of map markers by dynamically adjusting their colors based on the app's | ||||
|   theme. The map markers will now appear lighter when the app is in dark mode and darker when the | ||||
|   app is in light mode. This change aims to enhance marker visibility and improve the overall user | ||||
|   experience. | ||||
| - Added information on where user feedback is posted, helping users track existing feedback and | ||||
|   monitor their own submissions. | ||||
| - Enhanced the edit location screen of the upload screen by centering the map on the picture's | ||||
|   location from metadata when editing, or on the device's GPS location if metadata is unavailable, | ||||
|   improving accuracy and user experience. | ||||
| - Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a | ||||
|   recently uploaded image, enhancing clarity and user experience. | ||||
| - Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user | ||||
|   experience by showing loading progress until the image is fully loaded. | ||||
| - Fixed an issue where caption and description fields would intermittently disappear when using | ||||
|   voice input, ensuring text remains visible and stable across all entries. | ||||
| - Fixed a crash that occurred when attempting to remove multiple instances of caption/description | ||||
|   fields after initially adding them. | ||||
| - Improve the text in the prompt shown when skipping login to sound more natural. | ||||
| - Modified feedback addition logic to append new sections at the bottom of the page, ensuring | ||||
|   auto-archiving of sections functions correctly on the feedback page. | ||||
| - Resolved issue where the app failed to clear cookies upon logout. | ||||
| 
 | ||||
| ## v5.0.1 | ||||
| 
 | ||||
| Same as v5.0.0 except this fixes some R8 rules to ensure that the release | ||||
| variants of the app work as intended. | ||||
| 
 | ||||
| ## v5.0.0 | ||||
| 
 | ||||
| ### What's Changed | ||||
| 
 | ||||
| - Redesigned the map feature to **replace Mapbox with the osmdroid library**. | ||||
|   Key elements like pin visualization and user-centered display are still | ||||
|   included in this redesign. This is done to guard against possible misuse of | ||||
|   the Mapbox token and, more crucially, to keep the app from becoming dependent | ||||
|   on a service that charges for usage but offers a free tier. | ||||
| 
 | ||||
|   With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org). | ||||
| - Add the ability to **export locations of nearby missing pictures in GPX and | ||||
|   KML formats**. This allows users to browse the locations with desired radius | ||||
|   for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing | ||||
|   accessibility  and offline functionality. | ||||
| - **Limited the uploads via the custom image picker** to a maximum of 20. | ||||
| - Added two menu choices for **transparent image backgrounds**, giving users the | ||||
|   option of either a black or white background, increasing adaptability to | ||||
|   various theme settings. | ||||
| 
 | ||||
|   User customization option has been provided with the | ||||
|   ability to save background color selections permanently on a per image basis. | ||||
| - Implemented functionality to **automatically resume uploads** that become | ||||
|   stuck due to app termination or device reboot. | ||||
| - Added a **compass arrow in the Nearby banner** shown in the "Contributions" | ||||
|   screen to guide users towards the nearest item, thus providing the missing | ||||
|   directional cues. The arrow dynamically adjusts based on device rotation, | ||||
|   aligning with the calculated bearing towards the  target location. Further, | ||||
|   the distance and direction are updated as the user moves. | ||||
| - Implemented **voice input feature** for caption and description fields, | ||||
|   enabling users to dictate text directly into these fields. | ||||
| - Improved various flows in the app to **redirect users to the login page** and | ||||
|   display a  persistent message **if their session becomes invalid** due to a | ||||
|   password  change, enhancing user guidance and security measures. | ||||
| 
 | ||||
| ### Revamps and refactorings | ||||
| 
 | ||||
| - **Revamped initial upload screen layout and the description edit screen layout** | ||||
|   for enhanced user experience and ensuring better symmetry in the design. | ||||
| - **Replaced Butterknife with ViewBinding** in various places of the app. | ||||
| - Transferred essential code from **the redundant data-client module** to the | ||||
|   main Commons app code, enabling its integration and facilitating the removal | ||||
|   of the redundant module. Further, convert various parts of the code to Kotlin. | ||||
| - **Revamped the various location permission flows** to ensure consistency for | ||||
|   the sake of a better user experience. | ||||
| 
 | ||||
| ### Bug fixes and various changes | ||||
| 
 | ||||
| - Resolved an issue where paused uploads that were subsequently cancelled were | ||||
|   still being uploaded. | ||||
| - Fixed an issue where some user information such as upload count were not | ||||
|   displayed in the "Contributions" and "Profile" screens. | ||||
| - Fixed the long-standing broken *"Picture of the Day" widget* to restore its | ||||
|   usability. | ||||
| - Resolved an issue where some categories were hidden at the top of Upload | ||||
|   Wizard suggestions. | ||||
| - Resolved an issue where there was a grey empty screen at Upload wizard when | ||||
|   the app was denied the files permission. | ||||
| - Implemented logic to bypass media in Peer Review if the current reviewer is | ||||
|   also the user who uploaded the media. | ||||
| - Corrected arrow image behaviour in the first upload screen: now displays down | ||||
|   arrow when details card is fully visible, aligning with expected user | ||||
|   interaction. | ||||
| - Updated app icon to improve visibility and recognition on F-Droid. | ||||
| - Fixed issue causing all pictures to disappear and activity to reload fully in | ||||
|   the custom image selector after marking a picture as 'not for  upload', now | ||||
|   ensuring only the selected picture is removed as expected. | ||||
| 
 | ||||
| What's listed here is only a subset of all the changes. Check the full-list of | ||||
| the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0). | ||||
| Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0) | ||||
| for an exhaustive list of changes and the various contributors who contributed the same. | ||||
| 
 | ||||
| ## v4.2.1 | ||||
| 
 | ||||
| - Provide the ability to edit an image to losslessly rotate it while uploading | ||||
| - Fix a bug in v4.2.0 where the nearby places were not loading | ||||
| - Fix a bug where editing depictions was showing a progress bar indefinitely | ||||
| - In the upload screen, use different map icons to indicate if image is being uploaded with location | ||||
|   metadata | ||||
| - For nearby uploads, it is no longer possible to deselect the item's category and depiction | ||||
| - The Mapbox account key used by the app has been changed | ||||
| - Category search now shows exact matches without any discrepancies | ||||
| - Various bug and crash fixes | ||||
| 
 | ||||
| ## v4.2.0 | ||||
| - Dark mode colour improvements | ||||
| - Enhancements done to address location metadata loss including the metadata loss that occurs in | ||||
|   latest Android versions | ||||
| - Enhancements done to address the issue where uploads get stuck in queued state | ||||
| - Fix the inability to upload via the in-app camera option | ||||
| - Provide the ability to optionally include location metadata for in-app camera uploads in case the | ||||
|   device camera app does not provide location metadata | ||||
| - Use geo location URL that works consistently across all map applications | ||||
| - Fix crash when clicking on location target icon while trying to edit the location of an upload | ||||
| - Fix crash that occurs randomly while returning to the app after leaving it in the background | ||||
| - Fix crash in Sign up activity on Android version 5.0 and 5.1 | ||||
| - Android 13 compatibility changes | ||||
| 
 | ||||
| ## v4.1.0 | ||||
| - Location of pictures uploaded via custom picture selector are now recognized | ||||
| - Improvements to the custom picture selector | ||||
| - Ensure the WLM pictures are associated with the correct templates for each year | ||||
| - Only show pictures uploaded via app in peer review | ||||
| - Improve the variety of images show in peer review | ||||
| - Allow going to current location in location edit dialog while uploading a picture | ||||
| - Switch to using MapLibre instead of Mapbox and thereby disable telemetry sent to Mapbox | ||||
| - Fixed various bugs | ||||
| 
 | ||||
| ## v4.0.5 | ||||
| - Bumped min SDK to 29 to try and solve Google policy issue | ||||
| - Reverted dialog | ||||
| - Note: This encompasses versions 1031, 1032, and 1033, due to the Play Store's requirements to overwrite all the tracks with a post-fix version (otherwise no single track can be published) | ||||
| 
 | ||||
| ## v4.0.4 | ||||
| - Added dialog for Google's location policy | ||||
| 
 | ||||
| ## v4.0.3 | ||||
| - Added "Report" button for Google UGC policy | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							|  | @ -53,7 +53,6 @@ their contribution to the product. | |||
| * Butterknife | ||||
| * GSON | ||||
| * Timber | ||||
| * MapBox | ||||
| 
 | ||||
| 3rd party open source apps from which significant code has been reused: | ||||
| * Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia | ||||
|  |  | |||
							
								
								
									
										14
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
										
									
									
									
								
							|  | @ -6,7 +6,7 @@ | |||
| 
 | ||||
| The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2]. | ||||
| 
 | ||||
| Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)  | ||||
| Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request! :-)  | ||||
| 
 | ||||
| <a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank"> | ||||
| <img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a> | ||||
|  | @ -15,7 +15,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra | |||
| 
 | ||||
| ## Documentation | ||||
| 
 | ||||
| We try to have an extensive documentation at our [documentation repository][4]: | ||||
| Our [documentation repository][4] contains extensive documentation for users, contributors, and developers alike: | ||||
| 
 | ||||
| * [User Documentation][5] | ||||
| * [Contributor Documentation][6] | ||||
|  | @ -29,11 +29,11 @@ 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/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/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<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/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/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<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/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/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/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<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/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) | | ||||
| | [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<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/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) | | ||||
| | [<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/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/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/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/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) | | ||||
| 
 | ||||
| 
 | ||||
| .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). | ||||
|  |  | |||
							
								
								
									
										115
									
								
								app/build.gradle
									
										
									
									
									
								
							
							
						
						
									
										115
									
								
								app/build.gradle
									
										
									
									
									
								
							|  | @ -5,7 +5,7 @@ apply from: '../gitutils.gradle' | |||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'kotlin-android' | ||||
| apply plugin: 'kotlin-kapt' | ||||
| apply plugin: 'kotlin-android-extensions' | ||||
| apply plugin: 'kotlin-parcelize' | ||||
| apply from: "$rootDir/jacoco.gradle" | ||||
| 
 | ||||
| def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists() | ||||
|  | @ -16,13 +16,17 @@ if (isRunningOnTravisAndIsNotPRBuild) { | |||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     implementation project(':wikimedia-data-client') | ||||
|     // Utils | ||||
|     implementation 'in.yuvi:http.fluent:1.3' | ||||
|     implementation 'com.google.code.gson:gson:2.8.5' | ||||
|     implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"){ | ||||
|         force = true //API 19 support | ||||
|     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' | ||||
|  | @ -45,10 +49,8 @@ dependencies { | |||
|     implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' | ||||
|     implementation 'com.dinuscxj:circleprogressbar:1.1.1' | ||||
|     implementation 'com.karumi:dexter:5.0.0' | ||||
|     implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" | ||||
|     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' | ||||
| 
 | ||||
|     kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" | ||||
|     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" | ||||
|  | @ -73,30 +75,33 @@ dependencies { | |||
|     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" | ||||
|     annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" | ||||
| 
 | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" | ||||
| 
 | ||||
|     //Mocking | ||||
|     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' | ||||
|     testImplementation 'org.mockito:mockito-inline:2.13.0' | ||||
|     testImplementation 'org.mockito:mockito-core:2.25.1' | ||||
|     testImplementation "org.powermock:powermock-module-junit4:2.0.2" | ||||
|     testImplementation "org.powermock:powermock-api-mockito2:2.0.2" | ||||
|     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" | ||||
| 
 | ||||
|     // Unit testing | ||||
|     testImplementation 'junit:junit:4.13.2' | ||||
|     testImplementation 'org.robolectric:robolectric:4.6.1' | ||||
|     testImplementation 'androidx.test:core:1.4.0' | ||||
|     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.1.2" | ||||
|     testImplementation "androidx.arch.core:core-testing:2.1.0" | ||||
|     testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0" | ||||
|     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0" | ||||
|     testImplementation 'com.facebook.soloader:soloader:0.10.1' | ||||
|     testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" | ||||
|     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' | ||||
|  | @ -119,8 +124,7 @@ dependencies { | |||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||
|     implementation "androidx.exifinterface:exifinterface:1.3.2" | ||||
|     implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" | ||||
|     implementation "androidx.multidex:multidex:2.0.1" | ||||
|     compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1' | ||||
|     implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' | ||||
| 
 | ||||
|     //swipe_layout | ||||
|     implementation 'com.daimajia.swipelayout:library:1.2.0@aar' | ||||
|  | @ -131,7 +135,6 @@ dependencies { | |||
|     implementation "androidx.room:room-rxjava2:$ROOM_VERSION" | ||||
|     kapt "androidx.room:room-compiler:$ROOM_VERSION" | ||||
|     // For Kotlin use kapt instead of annotationProcessor | ||||
|     implementation 'com.squareup.retrofit2:retrofit:2.8.1' | ||||
|     testImplementation "androidx.arch.core:core-testing:2.1.0" | ||||
| 
 | ||||
|     // Pref | ||||
|  | @ -139,10 +142,12 @@ dependencies { | |||
|     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.0" | ||||
|     def work_version = "2.8.1" | ||||
|     // Kotlin + coroutines | ||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||
|     implementation("androidx.work:work-runtime:$work_version") | ||||
|  | @ -151,8 +156,21 @@ dependencies { | |||
|     //Glide | ||||
|     implementation 'com.github.bumptech.glide:glide:4.12.0' | ||||
|     annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' | ||||
|     kaptTest "androidx.databinding:databinding-compiler:8.0.2" | ||||
|     kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2" | ||||
| 
 | ||||
|     implementation("io.github.coordinates2country:coordinates2country-android:1.3") {  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) { | ||||
|  | @ -168,17 +186,17 @@ project.gradle.taskGraph.whenReady { | |||
| } | ||||
| 
 | ||||
| android { | ||||
|     compileSdkVersion 31 | ||||
|     compileSdkVersion 33 | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         //applicationId 'fr.free.nrw.commons' | ||||
| 
 | ||||
|         versionCode 1029 | ||||
|         versionName '4.0.3' | ||||
|         versionCode 1040 | ||||
|         versionName '5.0.2' | ||||
|         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) | ||||
| 
 | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 31 | ||||
|         targetSdkVersion 33 | ||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||
|         testInstrumentationRunnerArguments clearPackageData: 'true' | ||||
| 
 | ||||
|  | @ -188,17 +206,23 @@ android { | |||
| 
 | ||||
|         vectorDrawables.useSupportLibrary = true | ||||
|     } | ||||
| 
 | ||||
|     packagingOptions { | ||||
|         exclude 'META-INF/androidx.*' | ||||
|         exclude 'META-INF/proguard/androidx-annotations.pro' | ||||
|         jniLibs { | ||||
|             excludes += ['META-INF/androidx.*'] | ||||
|         } | ||||
|         resources { | ||||
|             excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro'] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     testOptions { | ||||
|         animationsDisabled true | ||||
| 
 | ||||
|         unitTests.returnDefaultValues = true | ||||
|         unitTests.includeAndroidResources = true | ||||
|         unitTests { | ||||
|             returnDefaultValues = true | ||||
|             includeAndroidResources = true | ||||
|         } | ||||
| 
 | ||||
|         unitTests.all { | ||||
|             jvmArgs '-noverify' | ||||
|  | @ -223,13 +247,14 @@ android { | |||
|             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 | ||||
|             testCoverageEnabled true | ||||
|             minifyEnabled false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' | ||||
|             testProguardFile 'test-proguard-rules.txt' | ||||
|             versionNameSuffix "-debug-" + getBranchName() | ||||
|  | @ -284,7 +309,6 @@ android { | |||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" | ||||
| 
 | ||||
|             dimension 'tier' | ||||
|         } | ||||
| 
 | ||||
|  | @ -320,20 +344,17 @@ android { | |||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" | ||||
| 
 | ||||
|             dimension 'tier' | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     lintOptions { | ||||
|         disable 'MissingTranslation' | ||||
|         disable 'ExtraTranslation' | ||||
|         abortOnError false | ||||
|     } | ||||
| 
 | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_1_8 | ||||
|         targetCompatibility JavaVersion.VERSION_1_8 | ||||
|         sourceCompatibility JavaVersion.VERSION_11 | ||||
|         targetCompatibility JavaVersion.VERSION_11 | ||||
|     } | ||||
|     kotlinOptions { | ||||
|         jvmTarget = "1.8" | ||||
|     } | ||||
| 
 | ||||
|     buildToolsVersion buildToolsVersion | ||||
|  | @ -341,7 +362,11 @@ android { | |||
|     buildFeatures { | ||||
|         viewBinding true | ||||
|     } | ||||
| 
 | ||||
|     namespace 'fr.free.nrw.commons' | ||||
|     lint { | ||||
|         abortOnError false | ||||
|         disable 'MissingTranslation', 'ExtraTranslation' | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| String getTestUserName() { | ||||
|  | @ -371,7 +396,3 @@ if (isRunningOnTravisAndIsNotPRBuild) { | |||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| androidExtensions { | ||||
|     experimental = true | ||||
| } | ||||
|  |  | |||
|  | @ -31,6 +31,17 @@ | |||
| -keepattributes Signature | ||||
| # Retain declared checked exceptions for use by a Proxy instance. | ||||
| -keepattributes Exceptions | ||||
| 
 | ||||
| # Note: The model package right now seems to include some other classes that | ||||
| # are not used for serialization / deserialization over Gson. Hopefully | ||||
| # that's not a problem since it only prevents R8 from avoiding trimming | ||||
| # of few more classes. | ||||
| -keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; } | ||||
| -keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; } | ||||
| -keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; } | ||||
| -keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; } | ||||
| -keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; } | ||||
| 
 | ||||
| # --- /Retrofit --- | ||||
| 
 | ||||
| # --- OkHttp + Okio --- | ||||
|  |  | |||
|  | @ -1,74 +0,0 @@ | |||
| package fr.free.nrw.commons; | ||||
| 
 | ||||
| import static org.hamcrest.CoreMatchers.equalTo; | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import androidx.room.Room; | ||||
| import androidx.test.core.app.ApplicationProvider; | ||||
| import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||
| import fr.free.nrw.commons.db.AppDatabase; | ||||
| import fr.free.nrw.commons.review.ReviewDao; | ||||
| import fr.free.nrw.commons.review.ReviewEntity; | ||||
| import org.junit.After; | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| 
 | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| public class ReviewDaoTest { | ||||
| 
 | ||||
|     private ReviewDao reviewDao; | ||||
|     private AppDatabase database; | ||||
| 
 | ||||
|     /** | ||||
|      * Set up the application database | ||||
|      */ | ||||
|     @Before | ||||
|     public void createDb() { | ||||
|         Context context = ApplicationProvider.getApplicationContext(); | ||||
|         database = Room.inMemoryDatabaseBuilder( | ||||
|                 context, AppDatabase.class) | ||||
|             .allowMainThreadQueries() | ||||
|             .build(); | ||||
|         reviewDao = database.ReviewDao(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the database | ||||
|      */ | ||||
|     @After | ||||
|     public void closeDb() { | ||||
|         database.close(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test insertion | ||||
|      * Also checks isReviewedAlready(): | ||||
|      * Case 1: When image has been reviewed/skipped by the user | ||||
|      */ | ||||
|     @Test | ||||
|     public void insert() { | ||||
|         // Insert data | ||||
|         String imageId = "1234"; | ||||
|         ReviewEntity reviewEntity = new ReviewEntity(imageId); | ||||
|         reviewDao.insert(reviewEntity); | ||||
| 
 | ||||
|         // Check insertion | ||||
|         // Covers the case where the image exists in the database | ||||
|         // And isReviewedAlready() returns true | ||||
|         Boolean isInserted = reviewDao.isReviewedAlready(imageId); | ||||
|         assertThat(isInserted, equalTo(true)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test review status of the image | ||||
|      * Case 2: When image has not been reviewed/skipped | ||||
|      */ | ||||
|     @Test | ||||
|     public void isReviewedAlready(){ | ||||
|         String imageId = "5856"; | ||||
|         Boolean isInserted = reviewDao.isReviewedAlready(imageId); | ||||
|         assertThat(isInserted, equalTo(false)); | ||||
|     } | ||||
| } | ||||
|  | @ -234,7 +234,7 @@ class UploadTest { | |||
|                         .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0, | ||||
|                                 MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"))) | ||||
| 
 | ||||
|         onView(withId(R.id.btn_add_description)) | ||||
|         onView(withId(R.id.btn_add)) | ||||
|                 .perform(click()) | ||||
| 
 | ||||
|         onView(withId(R.id.rv_descriptions)).perform( | ||||
|  |  | |||
|  | @ -1,256 +1,266 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="fr.free.nrw.commons"> | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> | ||||
|     <uses-permission android:name="android.permission.READ_SYNC_STATS" /> | ||||
|     <uses-permission android:name="android.permission.REORDER_TASKS" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> | ||||
|     <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||
|     <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||
|     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||
|     <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> | ||||
|     <uses-permission android:name="android.permission.SET_WALLPAPER"/> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> | ||||
|   xmlns:tools="http://schemas.android.com/tools"> | ||||
|   <uses-permission android:name="android.permission.INTERNET" /> | ||||
|   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> | ||||
|   <uses-permission android:name="android.permission.READ_SYNC_STATS" /> | ||||
|   <uses-permission android:name="android.permission.REORDER_TASKS" /> | ||||
|   <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> | ||||
|   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|   <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||
|   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||
|   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> | ||||
|   <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> | ||||
|   <uses-permission android:name="android.permission.SET_WALLPAPER"/> | ||||
|   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> | ||||
| 
 | ||||
|     <queries> | ||||
|         <!-- Browser --> | ||||
|         <intent> | ||||
|             <action android:name="android.intent.action.VIEW" /> | ||||
|             <category android:name="android.intent.category.BROWSABLE" /> | ||||
|             <data android:scheme="https" /> | ||||
|         </intent> | ||||
|         <!-- Google Maps --> | ||||
|         <package android:name="com.google.android.apps.maps" /> | ||||
|     </queries> | ||||
|   <queries> | ||||
|     <!-- Browser --> | ||||
|     <intent> | ||||
|       <action android:name="android.intent.action.VIEW" /> | ||||
|       <category android:name="android.intent.category.BROWSABLE" /> | ||||
|       <data android:scheme="https" /> | ||||
|     </intent> | ||||
|     <!-- Google Maps --> | ||||
|     <package android:name="com.google.android.apps.maps" /> | ||||
|   </queries> | ||||
| 
 | ||||
| 
 | ||||
|     <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||
|     <uses-feature android:name="android.hardware.location.gps" /> | ||||
|   <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||
|   <uses-feature android:name="android.hardware.location.gps" /> | ||||
| 
 | ||||
|     <application | ||||
|         android:name=".CommonsApplication" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:theme="@style/LightAppTheme" | ||||
|         android:largeHeap="true" | ||||
|         android:supportsRtl="true" | ||||
|         tools:replace="android:appComponentFactory" | ||||
|         android:appComponentFactory="commons" | ||||
|         android:requestLegacyExternalStorage = "true" | ||||
|         tools:ignore="GoogleAppIndexingWarning"> | ||||
|   <application | ||||
|     android:name=".CommonsApplication" | ||||
|     android:icon="@mipmap/ic_launcher" | ||||
|     android:label="@string/app_name" | ||||
|     android:theme="@style/LightAppTheme" | ||||
|     android:largeHeap="true" | ||||
|     android:supportsRtl="true" | ||||
|     tools:replace="android:appComponentFactory" | ||||
|     android:appComponentFactory="commons" | ||||
|     android:requestLegacyExternalStorage = "true" | ||||
|     tools:ignore="GoogleAppIndexingWarning"> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".description.DescriptionEditActivity" | ||||
|             android:exported="true" /> | ||||
|     <activity | ||||
|       android:name=".nearby.WikidataFeedback" | ||||
|       android:exported="false" /> | ||||
| 
 | ||||
|         <activity android:name="org.acra.dialog.CrashReportDialog" | ||||
|             android:process=":acra" | ||||
|             android:launchMode="singleInstance" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:finishOnTaskLaunch="true" /> | ||||
|     <activity | ||||
|       android:theme="@style/EditActivityTheme" | ||||
|       android:name=".description.DescriptionEditActivity" | ||||
|       android:exported="true" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".media.ZoomableActivity" | ||||
|             android:label="Zoomable Activity" | ||||
|             android:configChanges="screenSize|keyboard|orientation" | ||||
|             android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> | ||||
|     <activity | ||||
|       android:name=".edit.EditActivity" | ||||
|       android:exported="false" /> | ||||
| 
 | ||||
|         <activity android:name=".auth.LoginActivity" | ||||
|                   android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|     <activity android:name="org.acra.dialog.CrashReportDialog" | ||||
|       android:process=":acra" | ||||
|       android:launchMode="singleInstance" | ||||
|       android:excludeFromRecents="true" | ||||
|       android:finishOnTaskLaunch="true" /> | ||||
| 
 | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|             </intent-filter> | ||||
|     <activity | ||||
|       android:name=".media.ZoomableActivity" | ||||
|       android:label="Zoomable Activity" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> | ||||
| 
 | ||||
|             <meta-data android:name="android.app.shortcuts" | ||||
|                 android:resource="@xml/shortcuts" /> | ||||
|     <activity android:name=".auth.LoginActivity" | ||||
|       android:exported="true"> | ||||
|       <intent-filter> | ||||
|         <category android:name="android.intent.category.LAUNCHER" /> | ||||
| 
 | ||||
|         </activity> | ||||
|         <activity android:name=".WelcomeActivity" /> | ||||
|         <action android:name="android.intent.action.MAIN" /> | ||||
|       </intent-filter> | ||||
| 
 | ||||
|         <activity | ||||
|             android:hardwareAccelerated="false" | ||||
|             android:name=".upload.UploadActivity" | ||||
|             android:exported="true" | ||||
|             android:configChanges="orientation|screenSize|keyboard" | ||||
|             android:icon="@mipmap/ic_launcher" | ||||
|             android:label="@string/app_name" | ||||
|             android:windowSoftInputMode="adjustResize" | ||||
|             > | ||||
|             <intent-filter android:label="@string/intent_share_upload_label"> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|       <meta-data android:name="android.app.shortcuts" | ||||
|         android:resource="@xml/shortcuts" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|     </activity> | ||||
|     <activity android:name=".WelcomeActivity" /> | ||||
| 
 | ||||
|                 <data android:mimeType="image/*" /> | ||||
|                 <data android:mimeType="audio/ogg" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter android:label="@string/intent_share_upload_label"> | ||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
|     <activity | ||||
|       android:hardwareAccelerated="false" | ||||
|       android:name=".upload.UploadActivity" | ||||
|       android:exported="true" | ||||
|       android:configChanges="orientation|screenSize|keyboard" | ||||
|       android:icon="@mipmap/ic_launcher" | ||||
|       android:label="@string/app_name" | ||||
|       android:windowSoftInputMode="adjustResize" | ||||
|       > | ||||
|       <intent-filter android:label="@string/intent_share_upload_label"> | ||||
|         <action android:name="android.intent.action.SEND" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|         <category android:name="android.intent.category.DEFAULT" /> | ||||
| 
 | ||||
|                 <data android:mimeType="image/*" /> | ||||
|                 <data android:mimeType="audio/ogg" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".contributions.MainActivity" | ||||
|             android:icon="@mipmap/ic_launcher" | ||||
|             android:label="@string/app_name" | ||||
|             android:configChanges="screenSize|keyboard|orientation" /> | ||||
|         <activity | ||||
|             android:name=".settings.SettingsActivity" | ||||
|             android:label="@string/title_activity_settings" /> | ||||
|         <activity | ||||
|             android:name=".AboutActivity" | ||||
|             android:label="@string/title_activity_about" | ||||
|             android:parentActivityName=".contributions.MainActivity" /> | ||||
|         <data android:mimeType="image/*" /> | ||||
|         <data android:mimeType="audio/ogg" /> | ||||
|       </intent-filter> | ||||
|       <intent-filter android:label="@string/intent_share_upload_label"> | ||||
|         <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".auth.SignupActivity" | ||||
|             android:configChanges="orientation|screenLayout|screenSize" | ||||
|             android:label="@string/title_activity_signup" /> | ||||
|         <category android:name="android.intent.category.DEFAULT" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".notification.NotificationActivity" | ||||
|             android:label="@string/navigation_item_notification" /> | ||||
|         <data android:mimeType="image/*" /> | ||||
|         <data android:mimeType="audio/ogg" /> | ||||
|       </intent-filter> | ||||
|     </activity> | ||||
|     <activity | ||||
|       android:name=".contributions.MainActivity" | ||||
|       android:icon="@mipmap/ic_launcher" | ||||
|       android:label="@string/app_name" | ||||
|       android:configChanges="screenSize|keyboard|orientation" /> | ||||
|     <activity | ||||
|       android:name=".settings.SettingsActivity" | ||||
|       android:label="@string/title_activity_settings" /> | ||||
|     <activity | ||||
|       android:name=".AboutActivity" | ||||
|       android:label="@string/title_activity_about" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|         <activity android:name=".quiz.QuizActivity" | ||||
|             android:label="@string/quiz"/> | ||||
|     <activity | ||||
|       android:name=".auth.SignupActivity" | ||||
|       android:configChanges="orientation|screenLayout|screenSize" | ||||
|       android:label="@string/title_activity_signup" /> | ||||
| 
 | ||||
|         <activity android:name=".quiz.QuizResultActivity" | ||||
|             android:label="@string/result"/> | ||||
|     <activity | ||||
|       android:name=".notification.NotificationActivity" | ||||
|       android:label="@string/navigation_item_notification" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||
|             android:label="@string/title_activity_custom_selector" | ||||
|             android:configChanges="screenSize|keyboard|orientation" | ||||
|             android:parentActivityName=".contributions.MainActivity" /> | ||||
|     <activity android:name=".quiz.QuizActivity" | ||||
|       android:label="@string/quiz"/> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".category.CategoryDetailsActivity" | ||||
|             android:label="@string/title_activity_featured_images" | ||||
|             android:configChanges="screenSize|keyboard|orientation" | ||||
|             android:parentActivityName=".contributions.MainActivity" /> | ||||
|     <activity android:name=".quiz.QuizResultActivity" | ||||
|       android:label="@string/result"/> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||
|             android:label="@string/title_activity_featured_images" | ||||
|             android:configChanges="screenSize|keyboard|orientation" | ||||
|             android:parentActivityName=".contributions.MainActivity" /> | ||||
|     <activity | ||||
|       android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||
|       android:label="@string/title_activity_custom_selector" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".explore.SearchActivity" | ||||
|             android:label="@string/title_activity_search" | ||||
|             android:launchMode="singleTop" | ||||
|             android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|             android:parentActivityName=".contributions.MainActivity" | ||||
|             /> | ||||
|     <activity | ||||
|       android:name=".category.CategoryDetailsActivity" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".profile.ProfileActivity" | ||||
|             android:configChanges="orientation|screenSize|keyboard" | ||||
|             android:label="@string/Profile" /> | ||||
|     <activity | ||||
|       android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".review.ReviewActivity" | ||||
|             android:label="@string/title_activity_review" /> | ||||
|     <activity | ||||
|       android:name=".explore.SearchActivity" | ||||
|       android:label="@string/title_activity_search" | ||||
|       android:launchMode="singleTop" | ||||
|       android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|       android:parentActivityName=".contributions.MainActivity" | ||||
|       /> | ||||
| 
 | ||||
|         <activity | ||||
|           android:name=".LocationPicker.LocationPickerActivity" | ||||
|           android:label="Location Picker" /> | ||||
|     <activity | ||||
|       android:name=".profile.ProfileActivity" | ||||
|       android:configChanges="orientation|screenSize|keyboard" | ||||
|       android:label="@string/Profile" /> | ||||
| 
 | ||||
|         <service | ||||
|             android:name=".auth.WikiAccountAuthenticatorService" | ||||
|             android:exported="true" | ||||
|             android:process=":auth"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.accounts.AccountAuthenticator" /> | ||||
|             </intent-filter> | ||||
|             <meta-data | ||||
|                 android:name="android.accounts.AccountAuthenticator" | ||||
|                 android:resource="@xml/authenticator" /> | ||||
|         </service> | ||||
|     <activity | ||||
|       android:name=".review.ReviewActivity" | ||||
|       android:label="@string/title_activity_review" /> | ||||
| 
 | ||||
|         <service | ||||
|             android:name="org.acra.sender.SenderService" | ||||
|             android:exported="false" | ||||
|             android:process=":acra" /> | ||||
|     <activity | ||||
|       android:name=".LocationPicker.LocationPickerActivity" | ||||
|       android:label="Location Picker" /> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name=".filepicker.ExtendedFileProvider" | ||||
|             android:authorities="${applicationId}.provider" | ||||
|             android:exported="false" | ||||
|             android:grantUriPermissions="true"> | ||||
|             <meta-data | ||||
|                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|                 android:resource="@xml/provider_paths" /> | ||||
|         </provider> | ||||
|     <service | ||||
|       android:name=".auth.WikiAccountAuthenticatorService" | ||||
|       android:exported="true" | ||||
|       android:process=":auth"> | ||||
|       <intent-filter> | ||||
|         <action android:name="android.accounts.AccountAuthenticator" /> | ||||
|       </intent-filter> | ||||
|       <meta-data | ||||
|         android:name="android.accounts.AccountAuthenticator" | ||||
|         android:resource="@xml/authenticator" /> | ||||
|     </service> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name=".category.CategoryContentProvider" | ||||
|             android:authorities="${applicationId}.categories.contentprovider" | ||||
|             android:exported="false" | ||||
|             android:label="@string/provider_categories" | ||||
|             android:syncable="false" /> | ||||
|     <service | ||||
|       android:name="org.acra.sender.SenderService" | ||||
|       android:exported="false" | ||||
|       android:process=":acra" /> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||
|             android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||
|             android:exported="false" | ||||
|             android:label="@string/provider_searches" | ||||
|             android:syncable="false" /> | ||||
|     <provider | ||||
|       android:name=".filepicker.ExtendedFileProvider" | ||||
|       android:authorities="${applicationId}.provider" | ||||
|       android:exported="false" | ||||
|       android:grantUriPermissions="true"> | ||||
|       <meta-data | ||||
|         android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|         android:resource="@xml/provider_paths" /> | ||||
|     </provider> | ||||
| 
 | ||||
|         <provider | ||||
|           android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||
|           android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||
|           android:exported="false" | ||||
|           android:label="@string/provider_recent_languages" | ||||
|           android:syncable="false" /> | ||||
|     <provider | ||||
|       android:name=".category.CategoryContentProvider" | ||||
|       android:authorities="${applicationId}.categories.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_categories" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||
|             android:authorities="${applicationId}.bookmarks.contentprovider" | ||||
|             android:exported="false" | ||||
|             android:label="@string/provider_bookmarks" | ||||
|             android:syncable="false" /> | ||||
|     <provider | ||||
|       android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||
|       android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_searches" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||
|             android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||
|             android:exported="false" | ||||
|             android:label="@string/provider_bookmarks_location" | ||||
|             android:syncable="false" /> | ||||
|     <provider | ||||
|       android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||
|       android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_recent_languages" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|         <provider | ||||
|           android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||
|           android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||
|           android:exported="false" | ||||
|           android:label="@string/provider_bookmarks_location" | ||||
|           android:syncable="false" /> | ||||
|     <provider | ||||
|       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_bookmarks" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|       <receiver android:name=".widget.PicOfDayAppWidget" | ||||
|                 android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||
|             </intent-filter> | ||||
|     <provider | ||||
|       android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_bookmarks_location" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|             <meta-data | ||||
|                 android:name="android.appwidget.provider" | ||||
|                 android:resource="@xml/pic_of_day_app_widget_info" /> | ||||
|         </receiver> | ||||
|     <provider | ||||
|       android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_bookmarks_location" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|         <uses-library android:name="org.apache.http.legacy" android:required="false" /> | ||||
|     <receiver android:name=".widget.PicOfDayAppWidget" | ||||
|       android:exported="true"> | ||||
|       <intent-filter> | ||||
|         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||
|       </intent-filter> | ||||
| 
 | ||||
|     </application> | ||||
|       <meta-data | ||||
|         android:name="android.appwidget.provider" | ||||
|         android:resource="@xml/pic_of_day_app_widget_info" /> | ||||
|     </receiver> | ||||
| 
 | ||||
|     <uses-library android:name="org.apache.http.legacy" android:required="false" /> | ||||
| 
 | ||||
|   </application> | ||||
| 
 | ||||
| </manifest> | ||||
							
								
								
									
										64
									
								
								app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| package fr.free.nrw.commons | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.Canvas | ||||
| import android.graphics.drawable.BitmapDrawable | ||||
| import android.graphics.drawable.Drawable | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| 
 | ||||
| class BaseMarker { | ||||
|     private var _position: LatLng = LatLng(0.0, 0.0, 0f) | ||||
|     private var _title: String = "" | ||||
|     private var _place: Place = Place() | ||||
|     private var _icon: Bitmap? = null | ||||
| 
 | ||||
|     var position: LatLng | ||||
|         get() = _position | ||||
|         set(value) { | ||||
|             _position = value | ||||
|         } | ||||
|     var title: String | ||||
|         get() = _title | ||||
|         set(value) { | ||||
|             _title = value | ||||
|         } | ||||
| 
 | ||||
|     var place: Place | ||||
|         get() = _place | ||||
|         set(value) { | ||||
|             _place = value | ||||
|         } | ||||
|     var icon: Bitmap? | ||||
|         get() = _icon | ||||
|         set(value) { | ||||
|             _icon = value | ||||
|         } | ||||
| 
 | ||||
|     constructor() { | ||||
|     } | ||||
| 
 | ||||
|     fun fromResource(context: Context, drawableResId: Int) { | ||||
|         val drawable: Drawable = context.resources.getDrawable(drawableResId) | ||||
|         icon = if (drawable is BitmapDrawable) { | ||||
|             (drawable as BitmapDrawable).bitmap | ||||
|         } else { | ||||
|             val bitmap = Bitmap.createBitmap( | ||||
|                 drawable.intrinsicWidth, | ||||
|                 drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 | ||||
|             ) | ||||
|             val canvas = Canvas(bitmap) | ||||
|             drawable.setBounds(0, 0, canvas.width, canvas.height) | ||||
|             drawable.draw(canvas) | ||||
|             bitmap | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										33
									
								
								app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| package fr.free.nrw.commons | ||||
| 
 | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| 
 | ||||
| class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable { | ||||
| 
 | ||||
|     constructor(parcel: Parcel) : this( | ||||
|         parcel.readDouble(), | ||||
|         parcel.readDouble(), | ||||
|         parcel.readDouble() | ||||
|     ) | ||||
| 
 | ||||
|     override fun writeToParcel(parcel: Parcel, flags: Int) { | ||||
|         parcel.writeDouble(latitude) | ||||
|         parcel.writeDouble(longitude) | ||||
|         parcel.writeDouble(zoom) | ||||
|     } | ||||
| 
 | ||||
|     override fun describeContents(): Int { | ||||
|         return 0 | ||||
|     } | ||||
| 
 | ||||
|     companion object CREATOR : Parcelable.Creator<CameraPosition> { | ||||
|         override fun createFromParcel(parcel: Parcel): CameraPosition { | ||||
|             return CameraPosition(parcel) | ||||
|         } | ||||
| 
 | ||||
|         override fun newArray(size: Int): Array<CameraPosition?> { | ||||
|             return arrayOfNulls(size) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,86 +0,0 @@ | |||
| package fr.free.nrw.commons; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import org.wikipedia.AppAdapter; | ||||
| import org.wikipedia.dataclient.SharedPreferenceCookieManager; | ||||
| import org.wikipedia.dataclient.WikiSite; | ||||
| import org.wikipedia.json.GsonMarshaller; | ||||
| import org.wikipedia.json.GsonUnmarshaller; | ||||
| import org.wikipedia.login.LoginResult; | ||||
| 
 | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import okhttp3.OkHttpClient; | ||||
| 
 | ||||
| public class CommonsAppAdapter extends AppAdapter { | ||||
|     private final int DEFAULT_THUMB_SIZE = 640; | ||||
|     private final String COOKIE_STORE_NAME = "cookie_store"; | ||||
| 
 | ||||
|     private final SessionManager sessionManager; | ||||
|     private final JsonKvStore preferences; | ||||
| 
 | ||||
|     CommonsAppAdapter(@NonNull SessionManager sessionManager, @NonNull JsonKvStore preferences) { | ||||
|         this.sessionManager = sessionManager; | ||||
|         this.preferences = preferences; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getMediaWikiBaseUrl() { | ||||
|         return BuildConfig.COMMONS_URL; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getRestbaseUriFormat() { | ||||
|         return BuildConfig.COMMONS_URL; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) { | ||||
|         return OkHttpConnectionFactory.getClient(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getDesiredLeadImageDp() { | ||||
|         return DEFAULT_THUMB_SIZE; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isLoggedIn() { | ||||
|         return sessionManager.isUserLoggedIn(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getUserName() { | ||||
|         return sessionManager.getUserName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getPassword() { | ||||
|         return sessionManager.getPassword(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateAccount(@NonNull LoginResult result) { | ||||
|         sessionManager.updateAccount(result); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public SharedPreferenceCookieManager getCookies() { | ||||
|         if (!preferences.contains(COOKIE_STORE_NAME)) { | ||||
|             return null; | ||||
|         } | ||||
|         return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, | ||||
|                 preferences.getString(COOKIE_STORE_NAME, null)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setCookies(@NonNull SharedPreferenceCookieManager cookies) { | ||||
|         preferences.putString(COOKIE_STORE_NAME, GsonMarshaller.marshal(cookies)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean logErrorsInsteadOfCrashing() { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | @ -9,22 +9,22 @@ import static org.acra.ReportField.STACK_TRACE; | |||
| import static org.acra.ReportField.USER_COMMENT; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.app.NotificationChannel; | ||||
| import android.app.NotificationManager; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.database.sqlite.SQLiteException; | ||||
| import android.os.Build; | ||||
| import android.os.Process; | ||||
| import android.util.Log; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.multidex.BuildConfig; | ||||
| import androidx.multidex.MultiDexApplication; | ||||
| import com.facebook.drawee.backends.pipeline.Fresco; | ||||
| import com.facebook.imagepipeline.core.ImagePipeline; | ||||
| import com.facebook.imagepipeline.core.ImagePipelineConfig; | ||||
| import com.mapbox.mapboxsdk.Mapbox; | ||||
| import com.mapbox.mapboxsdk.WellKnownTileServer; | ||||
| import fr.free.nrw.commons.auth.LoginActivity; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; | ||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; | ||||
|  | @ -36,12 +36,14 @@ import fr.free.nrw.commons.contributions.ContributionDao; | |||
| import fr.free.nrw.commons.data.DBOpenHelper; | ||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.language.AppLanguageLookUpTable; | ||||
| import fr.free.nrw.commons.logging.FileLoggingTree; | ||||
| import fr.free.nrw.commons.logging.LogUtils; | ||||
| import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; | ||||
| import fr.free.nrw.commons.settings.Prefs; | ||||
| import fr.free.nrw.commons.upload.FileUtils; | ||||
| import fr.free.nrw.commons.utils.ConfigUtils; | ||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.internal.functions.Functions; | ||||
|  | @ -59,8 +61,6 @@ import org.acra.annotation.AcraCore; | |||
| import org.acra.annotation.AcraDialog; | ||||
| import org.acra.annotation.AcraMailSender; | ||||
| import org.acra.data.StringFormat; | ||||
| import org.wikipedia.AppAdapter; | ||||
| import org.wikipedia.language.AppLanguageLookUpTable; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| @AcraCore( | ||||
|  | @ -85,6 +85,9 @@ import timber.log.Timber; | |||
| 
 | ||||
| public class CommonsApplication extends MultiDexApplication { | ||||
| 
 | ||||
|     public static final String loginMessageIntentKey = "loginMessage"; | ||||
|     public static final String loginUsernameIntentKey = "loginUsername"; | ||||
| 
 | ||||
|     public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
|  | @ -95,6 +98,9 @@ public class CommonsApplication extends MultiDexApplication { | |||
|     @Named("default_preferences") | ||||
|     JsonKvStore defaultPrefs; | ||||
| 
 | ||||
|     @Inject | ||||
|     CommonsCookieJar cookieJar; | ||||
| 
 | ||||
|     @Inject | ||||
|     CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; | ||||
| 
 | ||||
|  | @ -137,10 +143,15 @@ public class CommonsApplication extends MultiDexApplication { | |||
|     ContributionDao contributionDao; | ||||
| 
 | ||||
|     /** | ||||
|      *  In-memory list of contributions whose uploads have been paused by the user | ||||
|      * In-memory list of contributions whose uploads have been paused by the user | ||||
|      */ | ||||
|     public static Map<String, Boolean> pauseUploads = new HashMap<>(); | ||||
| 
 | ||||
|     /** | ||||
|      * In-memory list of uploads that have been cancelled by the user | ||||
|      */ | ||||
|     public static HashSet<String> cancelledUploads = new HashSet<>(); | ||||
| 
 | ||||
|     /** | ||||
|      * Used to declare and initialize various components and dependencies | ||||
|      */ | ||||
|  | @ -150,15 +161,12 @@ public class CommonsApplication extends MultiDexApplication { | |||
| 
 | ||||
|         INSTANCE = this; | ||||
|         ACRA.init(this); | ||||
|         Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token), WellKnownTileServer.Mapbox); | ||||
| 
 | ||||
|         ApplicationlessInjection | ||||
|             .getInstance(this) | ||||
|             .getCommonsApplicationComponent() | ||||
|             .inject(this); | ||||
| 
 | ||||
|         AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs)); | ||||
| 
 | ||||
|         initTimber(); | ||||
| 
 | ||||
|         if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { | ||||
|  | @ -286,6 +294,7 @@ public class CommonsApplication extends MultiDexApplication { | |||
|         } | ||||
| 
 | ||||
|         sessionManager.logout() | ||||
|             .andThen(Completable.fromAction(() -> cookieJar.clear())) | ||||
|             .andThen(Completable.fromAction(() -> { | ||||
|                     Timber.d("All accounts have been removed"); | ||||
|                     clearImageCache(); | ||||
|  | @ -337,4 +346,96 @@ public class CommonsApplication extends MultiDexApplication { | |||
| 
 | ||||
|         void onLogoutComplete(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity | ||||
|      * with relevant intent parameters. It does not perform the actual logout operation. | ||||
|      */ | ||||
|     public static class BaseLogoutListener implements CommonsApplication.LogoutListener { | ||||
| 
 | ||||
|         Context ctx; | ||||
|         String loginMessage, userName; | ||||
| 
 | ||||
|         /** | ||||
|          * Constructor for BaseLogoutListener. | ||||
|          * | ||||
|          * @param ctx Application context | ||||
|          */ | ||||
|         public BaseLogoutListener(final Context ctx) { | ||||
|             this.ctx = ctx; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Constructor for BaseLogoutListener | ||||
|          * | ||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. | ||||
|          * @param loginMessage  Message to be displayed on the login page | ||||
|          * @param loginUsername Username to be pre-filled on the login page | ||||
|          */ | ||||
|         public BaseLogoutListener(final Context ctx, final String loginMessage, | ||||
|             final String loginUsername) { | ||||
|             this.ctx = ctx; | ||||
|             this.loginMessage = loginMessage; | ||||
|             this.userName = loginUsername; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onLogoutComplete() { | ||||
|             Timber.d("Logout complete callback received."); | ||||
|             final Intent loginIntent = new Intent(ctx, LoginActivity.class); | ||||
|             loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||
|                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
| 
 | ||||
|             if (loginMessage != null) { | ||||
|                 loginIntent.putExtra(loginMessageIntentKey, loginMessage); | ||||
|             } | ||||
|             if (userName != null) { | ||||
|                 loginIntent.putExtra(loginUsernameIntentKey, userName); | ||||
|             } | ||||
| 
 | ||||
|             ctx.startActivity(loginIntent); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This class is an extension of BaseLogoutListener, providing additional functionality or customization | ||||
|      * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. | ||||
|      */ | ||||
|     public static class ActivityLogoutListener extends BaseLogoutListener { | ||||
| 
 | ||||
|         Activity activity; | ||||
| 
 | ||||
| 
 | ||||
|         /** | ||||
|          * Constructor for ActivityLogoutListener. | ||||
|          * | ||||
|          * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. | ||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. | ||||
|          */ | ||||
|         public ActivityLogoutListener(final Activity activity, final Context ctx) { | ||||
|             super(ctx); | ||||
|             this.activity = activity; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Constructor for ActivityLogoutListener with additional parameters for the login screen. | ||||
|          * | ||||
|          * @param activity      The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. | ||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. | ||||
|          * @param loginMessage  Message to be displayed on the login page after logout. | ||||
|          * @param loginUsername Username to be pre-filled on the login page after logout. | ||||
|          */ | ||||
|         public ActivityLogoutListener(final Activity activity, final Context ctx, | ||||
|             final String loginMessage, final String loginUsername) { | ||||
|             super(activity, loginMessage, loginUsername); | ||||
|             this.activity = activity; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onLogoutComplete() { | ||||
|             super.onLogoutComplete(); | ||||
|             activity.finish(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,8 @@ package fr.free.nrw.commons.LocationPicker; | |||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.content.Intent; | ||||
| import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||
| import fr.free.nrw.commons.CameraPosition; | ||||
| import fr.free.nrw.commons.Media; | ||||
| 
 | ||||
| /** | ||||
|  * Helper class for starting the activity | ||||
|  | @ -52,6 +53,17 @@ public final class LocationPicker { | |||
|           return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Gets and puts media in intent | ||||
|          * @param media Media | ||||
|          * @return LocationPicker.IntentBuilder | ||||
|          */ | ||||
|         public LocationPicker.IntentBuilder media( | ||||
|                 final Media media) { | ||||
|               intent.putExtra(LocationPickerConstants.MEDIA, media); | ||||
|               return this; | ||||
|             } | ||||
| 
 | ||||
|         /** | ||||
|          * Gets and sets the activity | ||||
|          * @param activity Activity | ||||
|  |  | |||
|  | @ -1,20 +1,22 @@ | |||
| package fr.free.nrw.commons.LocationPicker; | ||||
| 
 | ||||
| import static com.mapbox.mapboxsdk.style.layers.Property.NONE; | ||||
| import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE; | ||||
| import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap; | ||||
| import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement; | ||||
| import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage; | ||||
| import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; | ||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; | ||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM; | ||||
| import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Intent; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.location.Location; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.graphics.Color; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.location.LocationManager; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.text.Html; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.Window; | ||||
| import android.view.animation.OvershootInterpolator; | ||||
|  | @ -28,52 +30,56 @@ import androidx.appcompat.app.ActionBar; | |||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.appcompat.widget.AppCompatTextView; | ||||
| import androidx.constraintlayout.widget.ConstraintLayout; | ||||
| import androidx.lifecycle.Observer; | ||||
| import androidx.lifecycle.ViewModelProvider; | ||||
| import androidx.core.app.ActivityCompat; | ||||
| import androidx.core.content.ContextCompat; | ||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||
| import com.mapbox.geojson.Point; | ||||
| import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||
| import com.mapbox.mapboxsdk.camera.CameraPosition.Builder; | ||||
| import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; | ||||
| import com.mapbox.mapboxsdk.geometry.LatLng; | ||||
| import com.mapbox.mapboxsdk.location.LocationComponent; | ||||
| import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions; | ||||
| import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback; | ||||
| import com.mapbox.mapboxsdk.location.engine.LocationEngineResult; | ||||
| import com.mapbox.mapboxsdk.location.modes.CameraMode; | ||||
| import com.mapbox.mapboxsdk.location.modes.RenderMode; | ||||
| import com.mapbox.mapboxsdk.location.permissions.PermissionsManager; | ||||
| import com.mapbox.mapboxsdk.maps.MapView; | ||||
| import com.mapbox.mapboxsdk.maps.MapboxMap; | ||||
| import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; | ||||
| import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener; | ||||
| import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; | ||||
| import com.mapbox.mapboxsdk.maps.Style; | ||||
| import com.mapbox.mapboxsdk.maps.UiSettings; | ||||
| import com.mapbox.mapboxsdk.style.layers.Layer; | ||||
| import com.mapbox.mapboxsdk.style.layers.SymbolLayer; | ||||
| import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; | ||||
| import fr.free.nrw.commons.MapStyle; | ||||
| import fr.free.nrw.commons.CameraPosition; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.Media; | ||||
| 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.csrf.CsrfTokenClient; | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; | ||||
| import fr.free.nrw.commons.coordinates.CoordinateEditHelper; | ||||
| import fr.free.nrw.commons.filepicker.Constants; | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| import org.osmdroid.tileprovider.tilesource.TileSourceFactory; | ||||
| import org.osmdroid.util.GeoPoint; | ||||
| import org.osmdroid.util.constants.GeoConstants; | ||||
| import org.osmdroid.views.CustomZoomButtonsController; | ||||
| import org.osmdroid.views.overlay.Marker; | ||||
| import org.osmdroid.views.overlay.Overlay; | ||||
| import org.osmdroid.views.overlay.ScaleDiskOverlay; | ||||
| import org.osmdroid.views.overlay.TilesOverlay; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| /** | ||||
|  * Helps to pick location and return the result with an intent | ||||
|  */ | ||||
| public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback, | ||||
|     OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> { | ||||
| 
 | ||||
| public class LocationPickerActivity extends BaseActivity implements | ||||
|     LocationPermissionCallback { | ||||
|     /** | ||||
|      * DROPPED_MARKER_LAYER_ID : id for layer | ||||
|      * coordinateEditHelper: helps to edit coordinates | ||||
|      */ | ||||
|     private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID"; | ||||
|     @Inject | ||||
|     CoordinateEditHelper coordinateEditHelper; | ||||
|     /** | ||||
|      * media : Media object | ||||
|      */ | ||||
|     private Media media; | ||||
|     /** | ||||
|      * cameraPosition : position of picker | ||||
|      */ | ||||
|  | @ -83,13 +89,9 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|      */ | ||||
|     private ImageView markerImage; | ||||
|     /** | ||||
|      * mapboxMap : map | ||||
|      * mapView : OSM Map | ||||
|      */ | ||||
|     private MapboxMap mapboxMap; | ||||
|     /** | ||||
|      * mapView : view of the map | ||||
|      */ | ||||
|     private MapView mapView; | ||||
|     private org.osmdroid.views.MapView mapView; | ||||
|     /** | ||||
|      * tvAttribution : credit | ||||
|      */ | ||||
|  | @ -98,14 +100,14 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|      * activity : activity key | ||||
|      */ | ||||
|     private String activity; | ||||
|     /** | ||||
|      * location : location | ||||
|      */ | ||||
|     private Location location; | ||||
|     /** | ||||
|      * modifyLocationButton : button for start editing location | ||||
|      */ | ||||
|     Button modifyLocationButton; | ||||
|     /** | ||||
|      * removeLocationButton : button to remove location metadata | ||||
|      */ | ||||
|     Button removeLocationButton; | ||||
|     /** | ||||
|      * showInMapButton : button for showing in map | ||||
|      */ | ||||
|  | @ -118,10 +120,6 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|      * fabCenterOnLocation: button for center on location; | ||||
|      */ | ||||
|     FloatingActionButton fabCenterOnLocation; | ||||
|     /** | ||||
|      * droppedMarkerLayer : Layer for static screen | ||||
|      */ | ||||
|     private Layer droppedMarkerLayer; | ||||
|     /** | ||||
|      * shadow : imageview of shadow | ||||
|      */ | ||||
|  | @ -141,19 +139,38 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|     @Named("default_preferences") | ||||
|     public | ||||
|     JsonKvStore applicationKvStore; | ||||
|     BasicKvStore store; | ||||
|     /** | ||||
|      * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly | ||||
|      */ | ||||
|     @Inject | ||||
|     SystemThemeUtils systemThemeUtils; | ||||
|     private boolean isDarkTheme; | ||||
|     private boolean moveToCurrentLocation; | ||||
| 
 | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
|     LocationPermissionsHelper locationPermissionsHelper; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     /** | ||||
|      * Constants | ||||
|      */ | ||||
|     private static final String CAMERA_POS = "cameraPosition"; | ||||
|     private static final String ACTIVITY = "activity"; | ||||
| 
 | ||||
| 
 | ||||
|     @SuppressLint("ClickableViewAccessibility") | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); | ||||
|         super.onCreate(savedInstanceState); | ||||
| 
 | ||||
|         isDarkTheme = systemThemeUtils.isDeviceInNightMode(); | ||||
|         moveToCurrentLocation = false; | ||||
|         store = new BasicKvStore(this, "LocationPermissions"); | ||||
| 
 | ||||
|         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); | ||||
|         final ActionBar actionBar = getSupportActionBar(); | ||||
|  | @ -166,12 +183,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|             cameraPosition = getIntent() | ||||
|                 .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); | ||||
|             activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY); | ||||
|             media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA); | ||||
|         }else{ | ||||
|             cameraPosition = savedInstanceState.getParcelable(CAMERA_POS); | ||||
|             activity = savedInstanceState.getString(ACTIVITY); | ||||
|             media = savedInstanceState.getParcelable("sMedia"); | ||||
|         } | ||||
| 
 | ||||
|         final LocationPickerViewModel viewModel = new ViewModelProvider(this) | ||||
|             .get(LocationPickerViewModel.class); | ||||
|         viewModel.getResult().observe(this, this); | ||||
| 
 | ||||
|         bindViews(); | ||||
|         addBackButtonListener(); | ||||
|         addPlaceSelectedButton(); | ||||
|  | @ -179,18 +196,57 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|         getToolbarUI(); | ||||
|         addCenterOnGPSButton(); | ||||
| 
 | ||||
|         org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(), | ||||
|             PreferenceManager.getDefaultSharedPreferences(getApplicationContext())); | ||||
| 
 | ||||
|         mapView.setTileSource(TileSourceFactory.WIKIMEDIA); | ||||
|         mapView.setTilesScaledToDpi(true); | ||||
|         mapView.setMultiTouchControls(true); | ||||
| 
 | ||||
|         org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( | ||||
|             "Referer", "http://maps.wikimedia.org/" | ||||
|         ); | ||||
|         mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); | ||||
|         mapView.getController().setZoom(ZOOM_LEVEL); | ||||
|         mapView.setOnTouchListener((v, event) -> { | ||||
|             if (event.getAction() == MotionEvent.ACTION_MOVE) { | ||||
|                 if (markerImage.getTranslationY() == 0) { | ||||
|                     markerImage.animate().translationY(-75) | ||||
|                         .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||
|                 } | ||||
|             } else if (event.getAction() == MotionEvent.ACTION_UP) { | ||||
|                 markerImage.animate().translationY(0) | ||||
|                     .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|         if ("UploadActivity".equals(activity)) { | ||||
|             placeSelectedButton.setVisibility(View.GONE); | ||||
|             modifyLocationButton.setVisibility(View.VISIBLE); | ||||
|             removeLocationButton.setVisibility(View.VISIBLE); | ||||
|             showInMapButton.setVisibility(View.VISIBLE); | ||||
|             largeToolbarText.setText(getResources().getString(R.string.image_location)); | ||||
|             smallToolbarText.setText(getResources(). | ||||
|                 getString(R.string.check_whether_location_is_correct)); | ||||
|             fabCenterOnLocation.setVisibility(View.GONE); | ||||
|             markerImage.setVisibility(View.GONE); | ||||
|             shadow.setVisibility(View.GONE); | ||||
|             assert cameraPosition != null; | ||||
|             showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(), | ||||
|                 cameraPosition.getLongitude())); | ||||
|         } | ||||
|         setupMapView(); | ||||
|          | ||||
|         mapView.onCreate(savedInstanceState); | ||||
|         mapView.getMapAsync(this); | ||||
|         if("UploadActivity".equals(activity)){ | ||||
|             if(mapView != null && mapView.getController() != null && cameraPosition != null){ | ||||
|                 GeoPoint cameraGeoPoint = new GeoPoint(cameraPosition.getLatitude(), | ||||
|                     cameraPosition.getLongitude()); | ||||
| 
 | ||||
|                 mapView.getController().setCenter(cameraGeoPoint); | ||||
|                 mapView.getController().animateTo(cameraGeoPoint); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -201,12 +257,26 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|         tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * For setting up Dark Theme | ||||
|      */ | ||||
|     private void darkThemeSetup() { | ||||
|         if (isDarkTheme) { | ||||
|             shadow.setColorFilter(Color.argb(255, 255, 255, 255)); | ||||
|             mapView.getOverlayManager().getTilesOverlay() | ||||
|                 .setColorFilter(TilesOverlay.INVERT_COLORS); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clicking back button destroy locationPickerActivity | ||||
|      */ | ||||
|     private void addBackButtonListener() { | ||||
|         final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button); | ||||
|         backButton.setOnClickListener(view -> finish()); | ||||
|         backButton.setOnClickListener(v -> { | ||||
|             finish(); | ||||
|         }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -217,21 +287,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|         markerImage = findViewById(R.id.location_picker_image_view_marker); | ||||
|         tvAttribution = findViewById(R.id.tv_attribution); | ||||
|         modifyLocationButton = findViewById(R.id.modify_location); | ||||
|         removeLocationButton = findViewById(R.id.remove_location); | ||||
|         showInMapButton = findViewById(R.id.show_in_map); | ||||
|         showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); | ||||
|         shadow = findViewById(R.id.location_picker_image_view_shadow); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Binds the listeners | ||||
|      */ | ||||
|     private void bindListeners() { | ||||
|         mapboxMap.addOnCameraMoveStartedListener( | ||||
|             this); | ||||
|         mapboxMap.addOnCameraIdleListener( | ||||
|             this); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets toolbar color | ||||
|      */ | ||||
|  | @ -242,49 +303,13 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|         toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Takes action when map is ready to show | ||||
|      * @param mapboxMap map | ||||
|      */ | ||||
|     @Override | ||||
|     public void onMapReady(final MapboxMap mapboxMap) { | ||||
|         this.mapboxMap = mapboxMap; | ||||
|         mapboxMap.setStyle(isDarkTheme ? MapStyle.DARK : MapStyle.STREETS, this::onStyleLoaded); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes dropped marker and layer | ||||
|      * Handles camera position based on options | ||||
|      * Enables location components | ||||
|      * | ||||
|      * @param style style | ||||
|      */ | ||||
|     private void onStyleLoaded(final Style style) { | ||||
|         if (modifyLocationButton.getVisibility() == View.VISIBLE) { | ||||
|             initDroppedMarker(style); | ||||
|             adjustCameraBasedOnOptions(); | ||||
|             enableLocationComponent(style); | ||||
|             if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) { | ||||
|                 final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id"); | ||||
|                 if (source != null) { | ||||
|                     source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(), | ||||
|                         cameraPosition.target.getLatitude())); | ||||
|                 } | ||||
|                 droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID); | ||||
|                 if (droppedMarkerLayer != null) { | ||||
|                     droppedMarkerLayer.setProperties(visibility(VISIBLE)); | ||||
|                     markerImage.setVisibility(View.GONE); | ||||
|                     shadow.setVisibility(View.GONE); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             adjustCameraBasedOnOptions(); | ||||
|             enableLocationComponent(style); | ||||
|             bindListeners(); | ||||
|         } | ||||
| 
 | ||||
|     private void setupMapView() { | ||||
|         adjustCameraBasedOnOptions(); | ||||
|         modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); | ||||
|         removeLocationButton.setOnClickListener(v -> onClickRemoveLocation()); | ||||
|         showInMapButton.setOnClickListener(v -> showInMap()); | ||||
|         darkThemeSetup(); | ||||
|         requestLocationPermissions(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -293,133 +318,70 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|     private void onClickModifyLocation() { | ||||
|         placeSelectedButton.setVisibility(View.VISIBLE); | ||||
|         modifyLocationButton.setVisibility(View.GONE); | ||||
|         removeLocationButton.setVisibility(View.GONE); | ||||
|         showInMapButton.setVisibility(View.GONE); | ||||
|         droppedMarkerLayer.setProperties(visibility(NONE)); | ||||
|         markerImage.setVisibility(View.VISIBLE); | ||||
|         shadow.setVisibility(View.VISIBLE); | ||||
|         largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); | ||||
|         smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); | ||||
|         bindListeners(); | ||||
|         fabCenterOnLocation.setVisibility(View.VISIBLE); | ||||
|         removeSelectedLocationMarker(); | ||||
|         if (cameraPosition != null && mapView != null) { | ||||
|             if (mapView.getController() != null) { | ||||
|                 mapView.getController().animateTo(new GeoPoint(cameraPosition.getLatitude(), | ||||
|                     cameraPosition.getLongitude())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles onclick event of removeLocationButton | ||||
|      */ | ||||
|     private void onClickRemoveLocation() { | ||||
|         DialogUtil.showAlertDialog(this, | ||||
|             getString(R.string.remove_location_warning_title), | ||||
|             getString(R.string.remove_location_warning_desc), | ||||
|             getString(R.string.continue_message), | ||||
|             getString(R.string.cancel), () -> removeLocationFromImage(), null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Method to remove the location from the picture | ||||
|      */ | ||||
|     private void removeLocationFromImage() { | ||||
|         if (media != null) { | ||||
|             compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() | ||||
|                     , media, "0.0", "0.0", "0.0f") | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(s -> { | ||||
|                     Timber.d("Coordinates are removed from the image"); | ||||
|                 })); | ||||
|         } | ||||
|         final Intent returningIntent = new Intent(); | ||||
|         setResult(AppCompatActivity.RESULT_OK, returningIntent); | ||||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show the location in map app | ||||
|      */ | ||||
|     public void showInMap(){ | ||||
|     public void showInMap() { | ||||
|         Utils.handleGeoCoordinates(this, | ||||
|             new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(), | ||||
|                 cameraPosition.target.getLongitude(), 0.0f)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize Dropped Marker and layer without showing | ||||
|      * @param loadedMapStyle style | ||||
|      */ | ||||
|     private void initDroppedMarker(@NonNull final Style loadedMapStyle) { | ||||
|         // Add the marker image to map | ||||
|         loadedMapStyle.addImage("dropped-icon-image", BitmapFactory.decodeResource( | ||||
|             getResources(), R.drawable.map_default_map_marker)); | ||||
|         loadedMapStyle.addSource(new GeoJsonSource("dropped-marker-source-id")); | ||||
|         loadedMapStyle.addLayer(new SymbolLayer(DROPPED_MARKER_LAYER_ID, | ||||
|             "dropped-marker-source-id").withProperties( | ||||
|             iconImage("dropped-icon-image"), | ||||
|             visibility(NONE), | ||||
|             iconAllowOverlap(true), | ||||
|             iconIgnorePlacement(true) | ||||
|         )); | ||||
|             new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(), | ||||
|                 mapView.getMapCenter().getLongitude(), 0.0f)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * move the location to the current media coordinates | ||||
|      */ | ||||
|     private void adjustCameraBasedOnOptions() { | ||||
|         mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Enables location components | ||||
|      * @param loadedMapStyle Style | ||||
|      */ | ||||
|     @SuppressWarnings( {"MissingPermission"}) | ||||
|     private void enableLocationComponent(@NonNull final Style loadedMapStyle) { | ||||
|         final UiSettings uiSettings = mapboxMap.getUiSettings(); | ||||
|         uiSettings.setAttributionEnabled(false); | ||||
| 
 | ||||
|         // Check if permissions are enabled and if not request | ||||
|         if (PermissionsManager.areLocationPermissionsGranted(this)) { | ||||
| 
 | ||||
|             // Get an instance of the component | ||||
|             final LocationComponent locationComponent = mapboxMap.getLocationComponent(); | ||||
| 
 | ||||
|             // Activate with options | ||||
|             locationComponent.activateLocationComponent( | ||||
|                 LocationComponentActivationOptions.builder(this, loadedMapStyle).build()); | ||||
| 
 | ||||
|             // Enable to make component visible | ||||
|             locationComponent.setLocationComponentEnabled(true); | ||||
| 
 | ||||
|             // Set the component's camera mode | ||||
|             locationComponent.setCameraMode(CameraMode.NONE); | ||||
| 
 | ||||
|             // Set the component's render mode | ||||
|             locationComponent.setRenderMode(RenderMode.NORMAL); | ||||
| 
 | ||||
|             // Get the component's location engine to receive user's last location | ||||
|             locationComponent.getLocationEngine().getLastLocation( | ||||
|                 new LocationEngineCallback<LocationEngineResult>() { | ||||
|                     @Override | ||||
|                     public void onSuccess(LocationEngineResult result) { | ||||
|                         location = result.getLastLocation(); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(@NonNull Exception exception) { | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
| 
 | ||||
|         if (cameraPosition != null) { | ||||
|             mapView.getController().setCenter(new GeoPoint(cameraPosition.getLatitude(), | ||||
|                 cameraPosition.getLongitude())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Acts on camera moving | ||||
|      * @param reason int | ||||
|      */ | ||||
|     @Override | ||||
|     public void onCameraMoveStarted(final int reason) { | ||||
|         Timber.v("Map camera has begun moving."); | ||||
|         if (markerImage.getTranslationY() == 0) { | ||||
|             markerImage.animate().translationY(-75) | ||||
|                 .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Acts on camera idle | ||||
|      */ | ||||
|     @Override | ||||
|     public void onCameraIdle() { | ||||
|         Timber.v("Map camera is now idling."); | ||||
|         markerImage.animate().translationY(0) | ||||
|             .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Takes action on camera position | ||||
|      * @param position position of picker | ||||
|      */ | ||||
|     @Override | ||||
|     public void onChanged(@Nullable CameraPosition position) { | ||||
|         if (position == null) { | ||||
|             position = new Builder() | ||||
|                 .target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(), | ||||
|                     mapboxMap.getCameraPosition().target.getLongitude())) | ||||
|                 .zoom(16).build(); | ||||
|         } | ||||
|         cameraPosition = position; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Select the preferable location | ||||
|      */ | ||||
|  | @ -434,35 +396,127 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|     void placeSelected() { | ||||
|         if (activity.equals("NoLocationUploadActivity")) { | ||||
|             applicationKvStore.putString(LAST_LOCATION, | ||||
|                 mapboxMap.getCameraPosition().target.getLatitude() | ||||
|                 mapView.getMapCenter().getLatitude() | ||||
|                     + "," | ||||
|                     + mapboxMap.getCameraPosition().target.getLongitude()); | ||||
|             applicationKvStore.putString(LAST_ZOOM, mapboxMap.getCameraPosition().zoom + ""); | ||||
|                     + mapView.getMapCenter().getLongitude()); | ||||
|             applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + ""); | ||||
|         } | ||||
|         final Intent returningIntent = new Intent(); | ||||
|         returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, | ||||
|             mapboxMap.getCameraPosition()); | ||||
|         setResult(AppCompatActivity.RESULT_OK, returningIntent); | ||||
| 
 | ||||
|         if (media == null) { | ||||
|             final Intent returningIntent = new Intent(); | ||||
|             returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, | ||||
|                 new CameraPosition(mapView.getMapCenter().getLatitude(), | ||||
|                     mapView.getMapCenter().getLongitude(), 14.0)); | ||||
|             setResult(AppCompatActivity.RESULT_OK, returningIntent); | ||||
|         } else { | ||||
|             updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()), | ||||
|                 String.valueOf(mapView.getMapCenter().getLongitude()), | ||||
|                 String.valueOf(0.0f)); | ||||
|         } | ||||
| 
 | ||||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetched coordinates are replaced with existing coordinates by a POST API call. | ||||
|      * @param Latitude to be added | ||||
|      * @param Longitude to be added | ||||
|      * @param Accuracy to be added | ||||
|      */ | ||||
|     public void updateCoordinates(final String Latitude, final String Longitude, | ||||
|         final String Accuracy) { | ||||
|         if (media == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             compositeDisposable.add( | ||||
|                 coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media, | ||||
|                         Latitude, Longitude, Accuracy) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe(s -> { | ||||
|                             Timber.d("Coordinates are added."); | ||||
|                         })); | ||||
|         } catch (Exception e) { | ||||
|             if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { | ||||
|                 final String username = sessionManager.getUserName(); | ||||
|                 final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( | ||||
|                     this, | ||||
|                     getString(R.string.invalid_login_message), | ||||
|                     username | ||||
|                 ); | ||||
| 
 | ||||
|                 CommonsApplication.getInstance().clearApplicationData( | ||||
|                     this, logoutListener); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Center the camera on the last saved location | ||||
|      */ | ||||
|     private void addCenterOnGPSButton(){ | ||||
|     private void addCenterOnGPSButton() { | ||||
|         fabCenterOnLocation = findViewById(R.id.center_on_gps); | ||||
|         fabCenterOnLocation.setOnClickListener(view -> getCenter()); | ||||
|         fabCenterOnLocation.setOnClickListener(view -> { | ||||
|             moveToCurrentLocation = true; | ||||
|             requestLocationPermissions(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Animate map to move to desired Latitude and Longitude | ||||
|      * Adds selected location marker on the map | ||||
|      */ | ||||
|     void getCenter() { | ||||
|         mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(),location.getLongitude()),15.0)); | ||||
|     private void showSelectedLocationMarker(GeoPoint point) { | ||||
|         Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker); | ||||
|         Marker marker = new Marker(mapView); | ||||
|         marker.setPosition(point); | ||||
|         marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); | ||||
|         marker.setIcon(icon); | ||||
|         marker.setInfoWindow(null); | ||||
|         mapView.getOverlays().add(marker); | ||||
|         mapView.invalidate(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes selected location marker from the map | ||||
|      */ | ||||
|     private void removeSelectedLocationMarker() { | ||||
|         List<Overlay> overlays = mapView.getOverlays(); | ||||
|         for (int i = 0; i < overlays.size(); i++) { | ||||
|             if (overlays.get(i) instanceof Marker) { | ||||
|                 Marker item = (Marker) overlays.get(i); | ||||
|                 if (cameraPosition.getLatitude() == item.getPosition().getLatitude() | ||||
|                     && cameraPosition.getLongitude() == item.getPosition().getLongitude()) { | ||||
|                     mapView.getOverlays().remove(i); | ||||
|                     mapView.invalidate(); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Center the map at user's current location | ||||
|      */ | ||||
|     private void requestLocationPermissions() { | ||||
|         locationPermissionsHelper = new LocationPermissionsHelper( | ||||
|             this, locationManager, this); | ||||
|         locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title, | ||||
|             R.string.upload_map_location_access); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStart() { | ||||
|         super.onStart(); | ||||
|         mapView.onStart(); | ||||
|     public void onRequestPermissionsResult(final int requestCode, | ||||
|         @NonNull final String[] permissions, | ||||
|         @NonNull final int[] grantResults) { | ||||
|         if (requestCode == Constants.RequestCodes.LOCATION | ||||
|             && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||
|             onLocationPermissionGranted(); | ||||
|         } else { | ||||
|             onLocationPermissionDenied(getString(R.string.upload_map_location_access)); | ||||
|         } | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -478,26 +532,102 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | |||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStop() { | ||||
|         super.onStop(); | ||||
|         mapView.onStop(); | ||||
|     public void onLocationPermissionDenied(String toastMessage) { | ||||
|         if (!ActivityCompat.shouldShowRequestPermissionRationale(this, | ||||
|             permission.ACCESS_FINE_LOCATION)) { | ||||
|             if (!locationPermissionsHelper.checkLocationPermission(this)) { | ||||
|                 if (store.getBoolean("isPermissionDenied", false)) { | ||||
|                     // means user has denied location permission twice or checked the "Don't show again" | ||||
|                     locationPermissionsHelper.showAppSettingsDialog(this, | ||||
|                         R.string.upload_map_location_access); | ||||
|                 } else { | ||||
|                     Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); | ||||
|                 } | ||||
|                 store.putBoolean("isPermissionDenied", true); | ||||
|             } | ||||
|         } else { | ||||
|             Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(final @NotNull Bundle outState) { | ||||
|     public void onLocationPermissionGranted() { | ||||
|         if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) { | ||||
|             if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||
|                 locationManager.requestLocationUpdatesFromProvider( | ||||
|                     LocationManager.NETWORK_PROVIDER); | ||||
|                 locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); | ||||
|                 getLocation(); | ||||
|             } else { | ||||
|                 getLocation(); | ||||
|                 locationPermissionsHelper.showLocationOffDialog(this, | ||||
|                     R.string.ask_to_turn_location_on_text); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets new location if locations services are on, else gets last location | ||||
|      */ | ||||
|     private void getLocation() { | ||||
|         fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation(); | ||||
|         if (currLocation != null) { | ||||
|             GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(), | ||||
|                 currLocation.getLongitude()); | ||||
|             addLocationMarker(currLocationGeopoint); | ||||
|             mapView.getController().setCenter(currLocationGeopoint); | ||||
|             mapView.getController().animateTo(currLocationGeopoint); | ||||
|             markerImage.setTranslationY(0); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addLocationMarker(GeoPoint geoPoint) { | ||||
|         if (moveToCurrentLocation) { | ||||
|             mapView.getOverlays().clear(); | ||||
|         } | ||||
|         ScaleDiskOverlay diskOverlay = | ||||
|             new ScaleDiskOverlay(this, | ||||
|                 geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); | ||||
|         Paint circlePaint = new Paint(); | ||||
|         circlePaint.setColor(Color.rgb(128, 128, 128)); | ||||
|         circlePaint.setStyle(Paint.Style.STROKE); | ||||
|         circlePaint.setStrokeWidth(2f); | ||||
|         diskOverlay.setCirclePaint2(circlePaint); | ||||
|         Paint diskPaint = new Paint(); | ||||
|         diskPaint.setColor(Color.argb(40, 128, 128, 128)); | ||||
|         diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); | ||||
|         diskOverlay.setCirclePaint1(diskPaint); | ||||
|         diskOverlay.setDisplaySizeMin(900); | ||||
|         diskOverlay.setDisplaySizeMax(1700); | ||||
|         mapView.getOverlays().add(diskOverlay); | ||||
|         org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( | ||||
|             mapView); | ||||
|         startMarker.setPosition(geoPoint); | ||||
|         startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, | ||||
|             org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); | ||||
|         startMarker.setIcon( | ||||
|             ContextCompat.getDrawable(this, R.drawable.current_location_marker)); | ||||
|         startMarker.setTitle("Your Location"); | ||||
|         startMarker.setTextLabelFontSize(24); | ||||
|         mapView.getOverlays().add(startMarker); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Saves the state of the activity | ||||
|      * @param outState Bundle | ||||
|      */ | ||||
|     @Override | ||||
|     public void onSaveInstanceState(@NonNull final Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         mapView.onSaveInstanceState(outState); | ||||
|     } | ||||
|         if(cameraPosition!=null){ | ||||
|             outState.putParcelable(CAMERA_POS, cameraPosition); | ||||
|         } | ||||
|         if(activity!=null){ | ||||
|             outState.putString(ACTIVITY, activity); | ||||
|         } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         mapView.onDestroy(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onLowMemory() { | ||||
|         super.onLowMemory(); | ||||
|         mapView.onLowMemory(); | ||||
|         if(media!=null){ | ||||
|             outState.putParcelable("sMedia", media); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| package fr.free.nrw.commons.LocationPicker; | ||||
| 
 | ||||
| import com.mapbox.mapboxsdk.maps.Style; | ||||
| 
 | ||||
| /** | ||||
|  * Constants need for location picking | ||||
|  */ | ||||
|  | @ -13,6 +11,9 @@ public final class LocationPickerConstants { | |||
|     public static final String MAP_CAMERA_POSITION | ||||
|         = "location.picker.cameraPosition"; | ||||
| 
 | ||||
|     public static final String MEDIA | ||||
|         = "location.picker.media"; | ||||
| 
 | ||||
| 
 | ||||
|     private LocationPickerConstants() { | ||||
|     } | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import android.app.Application; | |||
| import androidx.annotation.NonNull; | ||||
| import androidx.lifecycle.AndroidViewModel; | ||||
| import androidx.lifecycle.MutableLiveData; | ||||
| import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||
| import fr.free.nrw.commons.CameraPosition; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ public abstract class MapController { | |||
|     public class NearbyPlacesInfo { | ||||
|         public List<Place> placeList; // List of nearby places | ||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area | ||||
|         public LatLng curLatLng; // Current location when this places are populated | ||||
|         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 | ||||
|     } | ||||
|  | @ -23,7 +23,7 @@ public abstract class MapController { | |||
|     public class ExplorePlacesInfo { | ||||
|         public List<Place> explorePlaceList; // List of nearby places | ||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area | ||||
|         public LatLng curLatLng; // Current location when this places are populated | ||||
|         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 | ||||
|     } | ||||
|  |  | |||
|  | @ -1,12 +0,0 @@ | |||
| package fr.free.nrw.commons; | ||||
| 
 | ||||
| import com.mapbox.mapboxsdk.maps.Style; | ||||
| 
 | ||||
| /** | ||||
|  * Constants for various map styles | ||||
|  */ | ||||
| public final class MapStyle { | ||||
|     public static final String DARK = Style.getPredefinedStyle("Dark"); | ||||
|     public static final String OUTDOORS = Style.getPredefinedStyle("Outdoors"); | ||||
|     public static final String STREETS = Style.getPredefinedStyle("Streets"); | ||||
| } | ||||
|  | @ -2,9 +2,8 @@ package fr.free.nrw.commons | |||
| 
 | ||||
| import android.os.Parcelable | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| import org.wikipedia.dataclient.mwapi.MwQueryPage | ||||
| import org.wikipedia.page.PageTitle | ||||
| import kotlinx.parcelize.Parcelize | ||||
| import fr.free.nrw.commons.wikidata.model.page.PageTitle | ||||
| import java.util.* | ||||
| 
 | ||||
| @Parcelize | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien | |||
|         return Single.ambArray( | ||||
|             mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId) | ||||
|                 .onErrorResumeNext { Single.never() }, | ||||
|             mediaClient.getMedia(media.filename) | ||||
|             mediaClient.getMediaSuppressingErrors(media.filename) | ||||
|                 .onErrorResumeNext { Single.never() } | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| 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.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | @ -15,28 +15,26 @@ import okhttp3.Response; | |||
| import okhttp3.ResponseBody; | ||||
| import okhttp3.logging.HttpLoggingInterceptor; | ||||
| import okhttp3.logging.HttpLoggingInterceptor.Level; | ||||
| import org.wikipedia.dataclient.SharedPreferenceCookieManager; | ||||
| import org.wikipedia.dataclient.okhttp.HttpStatusException; | ||||
| 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; | ||||
|     @NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(), | ||||
|             CACHE_DIR_NAME), NET_CACHE_SIZE); | ||||
| 
 | ||||
|     @NonNull | ||||
|     private static final OkHttpClient CLIENT = createClient(); | ||||
|     public static OkHttpClient CLIENT; | ||||
| 
 | ||||
|     @NonNull public static OkHttpClient getClient() { | ||||
|     @NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) { | ||||
|         if (CLIENT == null) { | ||||
|             CLIENT = createClient(cookieJar); | ||||
|         } | ||||
|         return CLIENT; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private static OkHttpClient createClient() { | ||||
|     private static OkHttpClient createClient(final CommonsCookieJar cookieJar) { | ||||
|         return new OkHttpClient.Builder() | ||||
|                 .cookieJar(SharedPreferenceCookieManager.getInstance()) | ||||
|                 .cache(NET_CACHE) | ||||
|                 .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) | ||||
|  | @ -69,6 +67,8 @@ public final class OkHttpConnectionFactory { | |||
|     } | ||||
| 
 | ||||
|     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"); | ||||
| 
 | ||||
|  | @ -77,7 +77,16 @@ public final class OkHttpConnectionFactory { | |||
|         @Override | ||||
|         @NonNull | ||||
|         public Response intercept(@NonNull final Chain chain) throws IOException { | ||||
|             final Response rsp = chain.proceed(chain.request()); | ||||
|             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())) { | ||||
|  | @ -91,7 +100,12 @@ public final class OkHttpConnectionFactory { | |||
|                         } | ||||
|                     } | ||||
|                 } catch (final IOException e) { | ||||
|                     Timber.e(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; | ||||
|             } | ||||
|  | @ -111,4 +125,30 @@ public final class OkHttpConnectionFactory { | |||
| 
 | ||||
|     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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -10,18 +10,16 @@ import android.text.SpannableString; | |||
| import android.text.style.UnderlineSpan; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.browser.customtabs.CustomTabColorSchemeParams; | ||||
| import androidx.browser.customtabs.CustomTabsIntent; | ||||
| import androidx.core.content.ContextCompat; | ||||
| 
 | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import java.util.Calendar; | ||||
| import java.util.Date; | ||||
| import org.wikipedia.dataclient.WikiSite; | ||||
| import org.wikipedia.page.PageTitle; | ||||
| 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; | ||||
|  | @ -31,9 +29,6 @@ import fr.free.nrw.commons.settings.Prefs; | |||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| import static android.widget.Toast.LENGTH_SHORT; | ||||
| import static fr.free.nrw.commons.campaigns.CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE; | ||||
| 
 | ||||
| public class Utils { | ||||
| 
 | ||||
|     public static PageTitle getPageTitle(@NonNull String title) { | ||||
|  | @ -137,12 +132,6 @@ public class Utils { | |||
|      */ | ||||
|     public static void handleWebUrl(Context context, Uri url) { | ||||
|         Timber.d("Launching web url %s", url.toString()); | ||||
|         Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); | ||||
|         if (browserIntent.resolveActivity(context.getPackageManager()) == null) { | ||||
|             Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); | ||||
|             toast.show(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder() | ||||
|             .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package fr.free.nrw.commons; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.text.Html; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  |  | |||
|  | @ -0,0 +1,18 @@ | |||
| package fr.free.nrw.commons.actions | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwResponse | ||||
| 
 | ||||
| /** | ||||
|  * Response of the Thanks API. | ||||
|  * Context: | ||||
|  * The Commons Android app lets you thank other contributors who have uploaded a great picture. | ||||
|  * See https://www.mediawiki.org/wiki/Extension:Thanks | ||||
|  */ | ||||
| class MwThankPostResponse : MwResponse() { | ||||
|     var result: Result? = null | ||||
| 
 | ||||
|     inner class Result { | ||||
|         var success: Int? = null | ||||
|         var recipient: String? = null | ||||
|     } | ||||
| } | ||||
|  | @ -1,8 +1,10 @@ | |||
| package fr.free.nrw.commons.actions | ||||
| 
 | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.Single | ||||
| import org.wikipedia.csrf.CsrfTokenClient | ||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | ||||
| import timber.log.Timber | ||||
| 
 | ||||
| /** | ||||
|  * This class acts as a Client to facilitate wiki page editing | ||||
|  | @ -25,10 +27,48 @@ class PageEditClient( | |||
|      */ | ||||
|     fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> { | ||||
|         return try { | ||||
|             pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking) | ||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||
|             pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) | ||||
|                 .map { editResponse -> | ||||
|                         editResponse.edit()!!.editSucceeded() | ||||
|                     } | ||||
|         } catch (throwable: Throwable) { | ||||
|             Observable.just(false) | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new page with the given title, text, and summary. | ||||
|      * | ||||
|      * @param pageTitle The title of the page to be created. | ||||
|      * @param text      The content of the page in wikitext format. | ||||
|      * @param summary   The edit summary for the page creation. | ||||
|      * @return An observable that emits true if the page creation succeeded, false otherwise. | ||||
|      * @throws InvalidLoginTokenException If an invalid login token is encountered during the process. | ||||
|      */ | ||||
|     fun postCreate(pageTitle: String, text: String, summary: String): Observable<Boolean> { | ||||
|         return try { | ||||
|             pageEditInterface.postCreate( | ||||
|                 pageTitle, | ||||
|                 summary, | ||||
|                 text, | ||||
|                 "text/x-wiki", | ||||
|                 "wikitext", | ||||
|                 true, | ||||
|                 true, | ||||
|                 csrfTokenClient.getTokenBlocking() | ||||
|             ).map { editResponse -> | ||||
|                 editResponse.edit()!!.editSucceeded() | ||||
|             } | ||||
|         } catch (throwable: Throwable) { | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -41,10 +81,14 @@ class PageEditClient( | |||
|      */ | ||||
|     fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> { | ||||
|         return try { | ||||
|             pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking) | ||||
|             pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) | ||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||
|         } catch (throwable: Throwable) { | ||||
|             Observable.just(false) | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -57,13 +101,40 @@ class PageEditClient( | |||
|      */ | ||||
|     fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> { | ||||
|         return try { | ||||
|             pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking) | ||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||
|             pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) | ||||
|                 .map { editResponse -> editResponse.edit()?.editSucceeded() ?: false } | ||||
|         } catch (throwable: Throwable) { | ||||
|             Observable.just(false) | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Appends a new section to the wiki page | ||||
|      * @param pageTitle   Title of the page to edit | ||||
|      * @param sectionTitle Title of the new section that needs to be created | ||||
|      * @param sectionText  The page content that is to be added to the section | ||||
|      * @param summary     Edit summary | ||||
|      * @return whether the edit was successful | ||||
|      */ | ||||
|     fun createNewSection(pageTitle: String, sectionTitle: String, sectionText: String, summary: String): Observable<Boolean> { | ||||
|         return try { | ||||
|             pageEditInterface.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking()) | ||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||
|         } catch (throwable: Throwable) { | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Set new labels to Wikibase server of commons | ||||
|      * @param summary   Edit summary | ||||
|  | @ -76,9 +147,14 @@ class PageEditClient( | |||
|                     language: String, value: String) : Observable<Int>{ | ||||
|         return try { | ||||
|             pageEditInterface.postCaptions(summary, title, language, | ||||
|                 value, csrfTokenClient.tokenBlocking).map { it.success } | ||||
|                 value, csrfTokenClient.getTokenBlocking() | ||||
|             ).map { it.success } | ||||
|         } catch (throwable: Throwable) { | ||||
|             Observable.just(0) | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 throw throwable | ||||
|             } else { | ||||
|                 Observable.just(0) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| package fr.free.nrw.commons.actions | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX | ||||
| import fr.free.nrw.commons.wikidata.model.Entities | ||||
| import fr.free.nrw.commons.wikidata.model.edit.Edit | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.Single | ||||
| import org.wikipedia.dataclient.Service | ||||
| import org.wikipedia.dataclient.mwapi.MwQueryResponse | ||||
| import org.wikipedia.edit.Edit | ||||
| import org.wikipedia.wikidata.Entities | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import retrofit2.http.* | ||||
| 
 | ||||
| /** | ||||
|  | @ -27,7 +27,7 @@ interface PageEditInterface { | |||
|      */ | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(Service.MW_API_PREFIX + "action=edit") | ||||
|     @POST(MW_API_PREFIX + "action=edit") | ||||
|     fun postEdit( | ||||
|         @Field("title") title: String, | ||||
|         @Field("summary") summary: String, | ||||
|  | @ -36,6 +36,33 @@ interface PageEditInterface { | |||
|         @Field("token") token: String | ||||
|     ): Observable<Edit> | ||||
| 
 | ||||
|     /** | ||||
|      * This method creates or edits a page for nearby items. | ||||
|      * | ||||
|      * @param title           Title of the page to edit. Cannot be used together with pageid. | ||||
|      * @param summary         Edit summary. Also used as the section title when section=new and sectiontitle is not set. | ||||
|      * @param text            Text of the page. | ||||
|      * @param contentformat   Format of the content (e.g., "text/x-wiki"). | ||||
|      * @param contentmodel    Model of the content (e.g., "wikitext"). | ||||
|      * @param minor           Whether the edit is a minor edit. | ||||
|      * @param recreate        Whether to recreate the page if it does not exist. | ||||
|      * @param token           A "csrf" token. This should always be sent as the last field of form data. | ||||
|      */ | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(MW_API_PREFIX + "action=edit") | ||||
|     fun postCreate( | ||||
|         @Field("title") title: String, | ||||
|         @Field("summary") summary: String, | ||||
|         @Field("text") text: String, | ||||
|         @Field("contentformat") contentformat: String, | ||||
|         @Field("contentmodel") contentmodel: String, | ||||
|         @Field("minor") minor: Boolean, | ||||
|         @Field("recreate") recreate: Boolean, | ||||
|         // NOTE: This csrf shold always be sent as the last field of form data | ||||
|         @Field("token") token: String | ||||
|     ): Observable<Edit> | ||||
| 
 | ||||
|     /** | ||||
|      * This method posts such that the Content which the page | ||||
|      * has will be appended with the value being passed to the | ||||
|  | @ -47,7 +74,7 @@ interface PageEditInterface { | |||
|      */ | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(Service.MW_API_PREFIX + "action=edit") | ||||
|     @POST(MW_API_PREFIX + "action=edit") | ||||
|     fun postAppendEdit( | ||||
|         @Field("title") title: String, | ||||
|         @Field("summary") summary: String, | ||||
|  | @ -66,7 +93,7 @@ interface PageEditInterface { | |||
|      */ | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(Service.MW_API_PREFIX + "action=edit") | ||||
|     @POST(MW_API_PREFIX + "action=edit") | ||||
|     fun postPrependEdit( | ||||
|         @Field("title") title: String, | ||||
|         @Field("summary") summary: String, | ||||
|  | @ -74,10 +101,20 @@ interface PageEditInterface { | |||
|         @Field("token") token: String | ||||
|     ): Observable<Edit> | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(MW_API_PREFIX + "action=edit§ion=new") | ||||
|     fun postNewSection( | ||||
|         @Field("title") title: String, | ||||
|         @Field("summary") summary: String, | ||||
|         @Field("sectiontitle") sectionTitle: String, | ||||
|         @Field("text") sectionText: String, | ||||
|         @Field("token") token: String | ||||
|     ): Observable<Edit> | ||||
| 
 | ||||
|     @FormUrlEncoded | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2") | ||||
|     @POST(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2") | ||||
|     fun postCaptions( | ||||
|         @Field("summary") summary: String, | ||||
|         @Field("title") title: String, | ||||
|  | @ -91,10 +128,7 @@ interface PageEditInterface { | |||
|      * @param titles : Name of the file | ||||
|      * @return Single<MwQueryResult> | ||||
|      */ | ||||
|     @GET( | ||||
|         Service.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( | ||||
|         @Query("titles") title: String | ||||
|     ): Single<MwQueryResponse?> | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ package fr.free.nrw.commons.actions | |||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF | ||||
| import io.reactivex.Observable | ||||
| import org.wikipedia.csrf.CsrfTokenClient | ||||
| import org.wikipedia.dataclient.Service | ||||
| import org.wikipedia.dataclient.mwapi.MwPostResponse | ||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||
| import fr.free.nrw.commons.auth.login.LoginFailedException | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| import javax.inject.Singleton | ||||
|  | @ -17,7 +17,7 @@ import javax.inject.Singleton | |||
| @Singleton | ||||
| class ThanksClient @Inject constructor( | ||||
|     @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, | ||||
|     @param:Named("commons-service") private val service: Service | ||||
|     private val service: ThanksInterface | ||||
| ) { | ||||
|     /** | ||||
|      * Thanks a user for a particular revision | ||||
|  | @ -26,10 +26,22 @@ class ThanksClient @Inject constructor( | |||
|      */ | ||||
|     fun thank(revisionId: Long): Observable<Boolean> { | ||||
|         return try { | ||||
|             service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent) | ||||
|                 .map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 } | ||||
|         } catch (throwable: Throwable) { | ||||
|             Observable.just(false) | ||||
|             service.thank( | ||||
|                 revisionId.toString(),                      // Rev | ||||
|                 null,                                       // Log | ||||
|                 csrfTokenClient.getTokenBlocking(),              // Token | ||||
|                 CommonsApplication.getInstance().userAgent  // Source | ||||
|             ).map { | ||||
|                 mwThankPostResponse -> mwThankPostResponse.result?.success == 1 | ||||
|             } | ||||
|         } | ||||
|         catch (throwable: Throwable) { | ||||
|             if (throwable is InvalidLoginTokenException) { | ||||
|                 Observable.error(throwable) | ||||
|             } | ||||
|             else { | ||||
|                 Observable.just(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,24 @@ | |||
| package fr.free.nrw.commons.actions | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX | ||||
| import io.reactivex.Observable | ||||
| import retrofit2.http.Field | ||||
| import retrofit2.http.FormUrlEncoded | ||||
| import retrofit2.http.POST | ||||
| 
 | ||||
| /** | ||||
|  * Thanks API. | ||||
|  * Context: | ||||
|  * The Commons Android app lets you thank another contributor who has uploaded a great picture. | ||||
|  * See https://www.mediawiki.org/wiki/Extension:Thanks | ||||
|  */ | ||||
| interface ThanksInterface { | ||||
|     @FormUrlEncoded | ||||
|     @POST(MW_API_PREFIX + "action=thank") | ||||
|     fun thank( | ||||
|         @Field("rev") rev: String?, | ||||
|         @Field("log") log: String?, | ||||
|         @Field("token") token: String, | ||||
|         @Field("source") source: String? | ||||
|     ): Observable<MwThankPostResponse?> | ||||
| } | ||||
|  | @ -14,10 +14,8 @@ import android.view.MenuItem; | |||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.inputmethod.InputMethodManager; | ||||
| import android.widget.Button; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import android.widget.TextView; | ||||
| import androidx.annotation.ColorRes; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | @ -26,31 +24,20 @@ import androidx.appcompat.app.AlertDialog; | |||
| import androidx.appcompat.app.AppCompatDelegate; | ||||
| import androidx.core.app.NavUtils; | ||||
| import androidx.core.content.ContextCompat; | ||||
| 
 | ||||
| import com.google.android.material.textfield.TextInputLayout; | ||||
| 
 | ||||
| import fr.free.nrw.commons.auth.login.LoginClient; | ||||
| import fr.free.nrw.commons.auth.login.LoginResult; | ||||
| import fr.free.nrw.commons.databinding.ActivityLoginBinding; | ||||
| import fr.free.nrw.commons.utils.ActivityUtils; | ||||
| import java.util.Locale; | ||||
| import org.wikipedia.AppAdapter; | ||||
| import org.wikipedia.dataclient.ServiceFactory; | ||||
| import org.wikipedia.dataclient.WikiSite; | ||||
| import org.wikipedia.dataclient.mwapi.MwQueryResponse; | ||||
| import org.wikipedia.login.LoginClient; | ||||
| import org.wikipedia.login.LoginClient.LoginCallback; | ||||
| import org.wikipedia.login.LoginResult; | ||||
| import fr.free.nrw.commons.auth.login.LoginCallback; | ||||
| 
 | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import butterknife.OnClick; | ||||
| import butterknife.OnEditorAction; | ||||
| import butterknife.OnFocusChange; | ||||
| import fr.free.nrw.commons.BuildConfig; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.WelcomeActivity; | ||||
| import fr.free.nrw.commons.contributions.MainActivity; | ||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
|  | @ -58,25 +45,19 @@ import fr.free.nrw.commons.utils.ConfigUtils; | |||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| import static android.view.KeyEvent.KEYCODE_ENTER; | ||||
| import static android.view.View.VISIBLE; | ||||
| import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; | ||||
| import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE; | ||||
| import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey; | ||||
| import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey; | ||||
| 
 | ||||
| public class LoginActivity extends AccountAuthenticatorActivity { | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     @Named(NAMED_COMMONS_WIKI_SITE) | ||||
|     WikiSite commonsWikiSite; | ||||
| 
 | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     JsonKvStore applicationKvStore; | ||||
|  | @ -87,39 +68,16 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     @Inject | ||||
|     SystemThemeUtils systemThemeUtils; | ||||
| 
 | ||||
|     @BindView(R.id.login_button) | ||||
|     Button loginButton; | ||||
| 
 | ||||
|     @BindView(R.id.login_username) | ||||
|     EditText usernameEdit; | ||||
| 
 | ||||
|     @BindView(R.id.login_password) | ||||
|     EditText passwordEdit; | ||||
| 
 | ||||
|     @BindView(R.id.login_two_factor) | ||||
|     EditText twoFactorEdit; | ||||
| 
 | ||||
|     @BindView(R.id.error_message_container) | ||||
|     ViewGroup errorMessageContainer; | ||||
| 
 | ||||
|     @BindView(R.id.error_message) | ||||
|     TextView errorMessage; | ||||
| 
 | ||||
|     @BindView(R.id.login_credentials) | ||||
|     TextView loginCredentials; | ||||
| 
 | ||||
|     @BindView(R.id.two_factor_container) | ||||
|     TextInputLayout twoFactorContainer; | ||||
| 
 | ||||
|     private ActivityLoginBinding binding; | ||||
|     ProgressDialog progressDialog; | ||||
|     private AppCompatDelegate delegate; | ||||
|     private LoginTextWatcher textWatcher = new LoginTextWatcher(); | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
|     private Call<MwQueryResponse> loginToken; | ||||
|     final  String saveProgressDailog="ProgressDailog_state"; | ||||
|     final String saveErrorMessage ="errorMessage"; | ||||
|     final String saveUsername="username"; | ||||
|     final  String savePassword="password"; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|  | @ -133,31 +91,50 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         getDelegate().installViewFactory(); | ||||
|         getDelegate().onCreate(savedInstanceState); | ||||
| 
 | ||||
|         setContentView(R.layout.activity_login); | ||||
|         binding = ActivityLoginBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
| 
 | ||||
|         ButterKnife.bind(this); | ||||
|         String message = getIntent().getStringExtra(loginMessageIntentKey); | ||||
|         String username = getIntent().getStringExtra(loginUsernameIntentKey); | ||||
| 
 | ||||
|         usernameEdit.addTextChangedListener(textWatcher); | ||||
|         passwordEdit.addTextChangedListener(textWatcher); | ||||
|         twoFactorEdit.addTextChangedListener(textWatcher); | ||||
|         binding.loginUsername.addTextChangedListener(textWatcher); | ||||
|         binding.loginPassword.addTextChangedListener(textWatcher); | ||||
|         binding.loginTwoFactor.addTextChangedListener(textWatcher); | ||||
| 
 | ||||
|         binding.skipLogin.setOnClickListener(view -> skipLogin()); | ||||
|         binding.forgotPassword.setOnClickListener(view -> forgotPassword()); | ||||
|         binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked()); | ||||
|         binding.signUpButton.setOnClickListener(view -> signUp()); | ||||
|         binding.loginButton.setOnClickListener(view -> performLogin()); | ||||
| 
 | ||||
|         binding.loginPassword.setOnEditorActionListener(this::onEditorAction); | ||||
|         binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged); | ||||
| 
 | ||||
|         if (ConfigUtils.isBetaFlavour()) { | ||||
|             loginCredentials.setText(getString(R.string.login_credential)); | ||||
|             binding.loginCredentials.setText(getString(R.string.login_credential)); | ||||
|         } else { | ||||
|             loginCredentials.setVisibility(View.GONE); | ||||
|             binding.loginCredentials.setVisibility(View.GONE); | ||||
|         } | ||||
|         if (message != null) { | ||||
|             showMessage(message, R.color.secondaryDarkColor); | ||||
|         } | ||||
|         if (username != null) { | ||||
|             binding.loginUsername.setText(username); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @OnFocusChange(R.id.login_password) | ||||
|     /** | ||||
|      * Hides the keyboard if the user's focus is not on the password (hasFocus is false). | ||||
|      * @param view The keyboard | ||||
|      * @param hasFocus Set to true if the keyboard has focus | ||||
|      */ | ||||
|     void onPasswordFocusChanged(View view, boolean hasFocus) { | ||||
|         if (!hasFocus) { | ||||
|             ViewUtil.hideKeyboard(view); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @OnEditorAction(R.id.login_password) | ||||
|     boolean onEditorAction(int actionId, KeyEvent keyEvent) { | ||||
|         if (loginButton.isEnabled()) { | ||||
|     boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { | ||||
|         if (binding.loginButton.isEnabled()) { | ||||
|             if (actionId == IME_ACTION_DONE) { | ||||
|                 performLogin(); | ||||
|                 return true; | ||||
|  | @ -170,8 +147,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @OnClick(R.id.skip_login) | ||||
|     void skipLogin() { | ||||
|     protected void skipLogin() { | ||||
|         new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) | ||||
|                 .setMessage(R.string.skip_login_message) | ||||
|                 .setCancelable(false) | ||||
|  | @ -183,18 +159,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|                 .show(); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.forgot_password) | ||||
|     void forgotPassword() { | ||||
|     protected void forgotPassword() { | ||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.about_privacy_policy) | ||||
|     void onPrivacyPolicyClicked() { | ||||
|     protected void onPrivacyPolicyClicked() { | ||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.sign_up_button) | ||||
|     void signUp() { | ||||
|     protected void signUp() { | ||||
|         Intent intent = new Intent(this, SignupActivity.class); | ||||
|         startActivity(intent); | ||||
|     } | ||||
|  | @ -232,76 +205,65 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|         usernameEdit.removeTextChangedListener(textWatcher); | ||||
|         passwordEdit.removeTextChangedListener(textWatcher); | ||||
|         twoFactorEdit.removeTextChangedListener(textWatcher); | ||||
|         binding.loginUsername.removeTextChangedListener(textWatcher); | ||||
|         binding.loginPassword.removeTextChangedListener(textWatcher); | ||||
|         binding.loginTwoFactor.removeTextChangedListener(textWatcher); | ||||
|         delegate.onDestroy(); | ||||
|         if(null!=loginClient) { | ||||
|             loginClient.cancel(); | ||||
|         } | ||||
|         binding = null; | ||||
|         super.onDestroy(); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.login_button) | ||||
|     public void performLogin() { | ||||
|         Timber.d("Login to start!"); | ||||
|         final String username = usernameEdit.getText().toString(); | ||||
|         final String rawUsername = usernameEdit.getText().toString().trim(); | ||||
|         final String password = passwordEdit.getText().toString(); | ||||
|         String twoFactorCode = twoFactorEdit.getText().toString(); | ||||
|         final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString(); | ||||
|         final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString(); | ||||
|         final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString(); | ||||
| 
 | ||||
|         showLoggingProgressBar(); | ||||
|         doLogin(username, password, twoFactorCode); | ||||
|         loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(), | ||||
|             new LoginCallback() { | ||||
|                 @Override | ||||
|                 public void success(@NonNull LoginResult loginResult) { | ||||
|                     runOnUiThread(()->{ | ||||
|                         Timber.d("Login Success"); | ||||
|                         hideProgress(); | ||||
|                         onLoginSuccess(loginResult); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) { | ||||
|                     runOnUiThread(()->{ | ||||
|                         Timber.d("Requesting 2FA prompt"); | ||||
|                         hideProgress(); | ||||
|                         askUserForTwoFactorAuth(); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void passwordResetPrompt(@Nullable String token) { | ||||
|                     runOnUiThread(()->{ | ||||
|                         Timber.d("Showing password reset prompt"); | ||||
|                         hideProgress(); | ||||
|                         showPasswordResetPrompt(); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void error(@NonNull Throwable caught) { | ||||
|                     runOnUiThread(()->{ | ||||
|                         Timber.e(caught); | ||||
|                         hideProgress(); | ||||
|                         showMessageAndCancelDialog(caught.getLocalizedMessage()); | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     private void doLogin(String username, String password, String twoFactorCode) { | ||||
|         progressDialog.show(); | ||||
|         loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken(); | ||||
|         loginToken.enqueue( | ||||
|                 new Callback<MwQueryResponse>() { | ||||
|                     @Override | ||||
|                     public void onResponse(Call<MwQueryResponse> call, | ||||
|                                            Response<MwQueryResponse> response) { | ||||
|                         loginClient.login(commonsWikiSite, username, password, null, twoFactorCode, | ||||
|                                 response.body().query().loginToken(), Locale.getDefault().getLanguage(), new LoginCallback() { | ||||
|                                     @Override | ||||
|                                     public void success(@NonNull LoginResult result) { | ||||
|                                         Timber.d("Login Success"); | ||||
|                                         onLoginSuccess(result); | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void twoFactorPrompt(@NonNull Throwable caught, | ||||
|                                                                 @Nullable String token) { | ||||
|                                         Timber.d("Requesting 2FA prompt"); | ||||
|                                         hideProgress(); | ||||
|                                         askUserForTwoFactorAuth(); | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void passwordResetPrompt(@Nullable String token) { | ||||
|                                         Timber.d("Showing password reset prompt"); | ||||
|                                         hideProgress(); | ||||
|                                         showPasswordResetPrompt(); | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void error(@NonNull Throwable caught) { | ||||
|                                         Timber.e(caught); | ||||
|                                         hideProgress(); | ||||
|                                         showMessageAndCancelDialog(caught.getLocalizedMessage()); | ||||
|                                     } | ||||
|                                 }); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onFailure(Call<MwQueryResponse> call, Throwable t) { | ||||
|                         Timber.e(t); | ||||
|                         showMessageAndCancelDialog(t.getLocalizedMessage()); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void hideProgress() { | ||||
|         progressDialog.dismiss(); | ||||
|  | @ -332,13 +294,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     } | ||||
| 
 | ||||
|     private void onLoginSuccess(LoginResult loginResult) { | ||||
|         if (!progressDialog.isShowing()) { | ||||
|             // no longer attached to activity! | ||||
|             return; | ||||
|         } | ||||
|         compositeDisposable.clear(); | ||||
|         sessionManager.setUserLoggedIn(true); | ||||
|         AppAdapter.get().updateAccount(loginResult); | ||||
|         sessionManager.updateAccount(loginResult); | ||||
|         progressDialog.dismiss(); | ||||
|         showSuccessAndDismissDialog(); | ||||
|         startMainActivity(); | ||||
|  | @ -385,9 +343,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
| 
 | ||||
|     public void askUserForTwoFactorAuth() { | ||||
|         progressDialog.dismiss(); | ||||
|         twoFactorContainer.setVisibility(VISIBLE); | ||||
|         twoFactorEdit.setVisibility(VISIBLE); | ||||
|         twoFactorEdit.requestFocus(); | ||||
|         binding.twoFactorContainer.setVisibility(VISIBLE); | ||||
|         binding.loginTwoFactor.setVisibility(VISIBLE); | ||||
|         binding.loginTwoFactor.requestFocus(); | ||||
|         InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
|         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); | ||||
|         showMessageAndCancelDialog(R.string.login_failed_2fa_needed); | ||||
|  | @ -418,15 +376,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|     } | ||||
| 
 | ||||
|     private void showMessage(@StringRes int resId, @ColorRes int colorResId) { | ||||
|         errorMessage.setText(getString(resId)); | ||||
|         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||
|         errorMessageContainer.setVisibility(VISIBLE); | ||||
|         binding.errorMessage.setText(getString(resId)); | ||||
|         binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||
|         binding.errorMessageContainer.setVisibility(VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     private void showMessage(String message, @ColorRes int colorResId) { | ||||
|         errorMessage.setText(message); | ||||
|         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||
|         errorMessageContainer.setVisibility(VISIBLE); | ||||
|         binding.errorMessage.setText(message); | ||||
|         binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||
|         binding.errorMessageContainer.setVisibility(VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     private AppCompatDelegate getDelegate() { | ||||
|  | @ -447,9 +405,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
| 
 | ||||
|         @Override | ||||
|         public void afterTextChanged(Editable editable) { | ||||
|             boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 | ||||
|                     && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE); | ||||
|             loginButton.setEnabled(enabled); | ||||
|             boolean enabled = binding.loginUsername.getText().length() != 0 && | ||||
|                 binding.loginPassword.getText().length() != 0 && | ||||
|                 (BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 || | ||||
|                     binding.loginTwoFactor.getVisibility() != VISIBLE); | ||||
|             binding.loginButton.setEnabled(enabled); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -467,22 +427,22 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         } else { | ||||
|             outState.putBoolean(saveProgressDailog,false); | ||||
|         } | ||||
|         outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage | ||||
|         outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage | ||||
|         outState.putString(saveUsername,getUsername()); // Save the username | ||||
|         outState.putString(savePassword,getPassword()); // Save the password | ||||
|     } | ||||
|     private String getUsername() { | ||||
|         return usernameEdit.getText().toString(); | ||||
|         return binding.loginUsername.getText().toString(); | ||||
|     } | ||||
|     private String getPassword(){ | ||||
|         return  passwordEdit.getText().toString(); | ||||
|         return  binding.loginPassword.getText().toString(); | ||||
|   } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onRestoreInstanceState(final Bundle savedInstanceState) { | ||||
|         super.onRestoreInstanceState(savedInstanceState); | ||||
|         usernameEdit.setText(savedInstanceState.getString(saveUsername)); | ||||
|         passwordEdit.setText(savedInstanceState.getString(savePassword)); | ||||
|         binding.loginUsername.setText(savedInstanceState.getString(saveUsername)); | ||||
|         binding.loginPassword.setText(savedInstanceState.getString(savePassword)); | ||||
|         if(savedInstanceState.getBoolean(saveProgressDailog)) { | ||||
|             performLogin(); | ||||
|         } | ||||
|  |  | |||
|  | @ -1,36 +0,0 @@ | |||
| package fr.free.nrw.commons.auth; | ||||
| 
 | ||||
| 
 | ||||
| import org.wikipedia.dataclient.Service; | ||||
| import org.wikipedia.dataclient.mwapi.MwPostResponse; | ||||
| 
 | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
| 
 | ||||
| import io.reactivex.Observable; | ||||
| 
 | ||||
| /** | ||||
|  * Handler for logout | ||||
|  */ | ||||
| @Singleton | ||||
| public class LogoutClient { | ||||
| 
 | ||||
|     private final Service service; | ||||
| 
 | ||||
|     @Inject | ||||
|     public LogoutClient(@Named("commons-service") Service service) { | ||||
|         this.service = service; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the  CSRF token and uses that to post the logout api call | ||||
|      * @return | ||||
|      */ | ||||
|     public Observable<MwPostResponse> postLogout() { | ||||
|         return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout( | ||||
|                 Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken()))); | ||||
|     } | ||||
| } | ||||
|  | @ -9,8 +9,7 @@ import android.text.TextUtils; | |||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.wikipedia.login.LoginResult; | ||||
| 
 | ||||
| import fr.free.nrw.commons.auth.login.LoginResult; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
|  | @ -123,18 +122,18 @@ public class SessionManager { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 1. Clears existing accounts from account manager | ||||
|      * 2. Calls MediaWikiApi's logout function to clear cookies | ||||
|      * @return | ||||
|      * Returns a Completable that clears existing accounts from account manager | ||||
|      */ | ||||
|     public Completable logout() { | ||||
|         AccountManager accountManager = AccountManager.get(context); | ||||
|         Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); | ||||
|         return Completable.fromObservable(Observable.fromArray(allAccounts) | ||||
|                 .map(a -> accountManager.removeAccount(a, null, null).getResult())) | ||||
|                 .doOnComplete(() -> { | ||||
|                     currentAccount = null; | ||||
|                 }); | ||||
|         return Completable.fromObservable( | ||||
|             Observable.empty() | ||||
|                       .doOnComplete( | ||||
|                           () -> { | ||||
|                               removeAccount(); | ||||
|                               currentAccount = null; | ||||
|                           } | ||||
|                       ) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| package fr.free.nrw.commons.auth; | ||||
| 
 | ||||
| import android.content.res.Configuration; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.webkit.WebSettings; | ||||
| import android.webkit.WebView; | ||||
|  | @ -61,4 +63,20 @@ public class SignupActivity extends BaseActivity { | |||
|             super.onBackPressed(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Known bug in androidx.appcompat library version 1.1.0 being tracked here | ||||
|      * https://issuetracker.google.com/issues/141132133 | ||||
|      * App tries to put light/dark theme to webview and crashes in the process | ||||
|      * This code tries to prevent applying the theme when sdk is between api 21 to 25 | ||||
|      * @param overrideConfiguration | ||||
|      */ | ||||
|     @Override | ||||
|     public void applyOverrideConfiguration(final Configuration overrideConfiguration) { | ||||
|         if (Build.VERSION.SDK_INT <= 25 && | ||||
|             (getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) { | ||||
|             return; | ||||
|         } | ||||
|         super.applyOverrideConfiguration(overrideConfiguration); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,170 @@ | |||
| package fr.free.nrw.commons.auth.csrf | ||||
| 
 | ||||
| import androidx.annotation.VisibleForTesting | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import fr.free.nrw.commons.auth.login.LoginClient | ||||
| import fr.free.nrw.commons.auth.login.LoginCallback | ||||
| import fr.free.nrw.commons.auth.login.LoginFailedException | ||||
| import fr.free.nrw.commons.auth.login.LoginResult | ||||
| import retrofit2.Call | ||||
| import retrofit2.Response | ||||
| import timber.log.Timber | ||||
| import java.util.concurrent.Callable | ||||
| import java.util.concurrent.Executors.newSingleThreadExecutor | ||||
| 
 | ||||
| class CsrfTokenClient( | ||||
|     private val sessionManager: SessionManager, | ||||
|     private val csrfTokenInterface: CsrfTokenInterface, | ||||
|     private val loginClient: LoginClient, | ||||
|     private val logoutClient: LogoutClient | ||||
| ) { | ||||
|     private var retries = 0 | ||||
|     private var csrfTokenCall: Call<MwQueryResponse?>? = null | ||||
| 
 | ||||
| 
 | ||||
|     @Throws(Throwable::class) | ||||
|     fun getTokenBlocking(): String { | ||||
|         var token = "" | ||||
|         val userName = sessionManager.userName ?: "" | ||||
|         val password = sessionManager.password ?: "" | ||||
| 
 | ||||
|         for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) { | ||||
|             try { | ||||
|                 if (retry > 0) { | ||||
|                     // Log in explicitly | ||||
|                     loginClient.loginBlocking(userName, password, "") | ||||
|                 } | ||||
| 
 | ||||
|                 // Get CSRFToken response off the main thread. | ||||
|                 val response = newSingleThreadExecutor().submit(Callable { | ||||
|                     csrfTokenInterface.getCsrfTokenCall().execute() | ||||
|                 }).get() | ||||
| 
 | ||||
|                 if (response.body()?.query()?.csrfToken().isNullOrEmpty()) { | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 token = response.body()!!.query()!!.csrfToken()!! | ||||
|                 if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) { | ||||
|                     throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) | ||||
|                 } | ||||
|                 break | ||||
|             } catch (e: LoginFailedException) { | ||||
|                throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) | ||||
|             } | ||||
|             catch (t: Throwable) { | ||||
|                 Timber.w(t) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (token.isEmpty() || token == ANON_TOKEN) { | ||||
|             throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) | ||||
|         } | ||||
|         return token | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> = | ||||
|         requestToken(service, object : Callback { | ||||
|             override fun success(token: String?) { | ||||
|                 if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) { | ||||
|                     retryWithLogin(cb) { | ||||
|                         InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) | ||||
|                     } | ||||
|                 } else { | ||||
|                     cb.success(token) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } | ||||
| 
 | ||||
|             override fun twoFactorPrompt() = cb.twoFactorPrompt() | ||||
|         }) | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> { | ||||
|         val call = service.getCsrfTokenCall() | ||||
|         call.enqueue(object : retrofit2.Callback<MwQueryResponse?> { | ||||
|             override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) { | ||||
|                 if (call.isCanceled) { | ||||
|                     return | ||||
|                 } | ||||
|                 cb.success(response.body()!!.query()!!.csrfToken()) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) { | ||||
|                 if (call.isCanceled) { | ||||
|                     return | ||||
|                 } | ||||
|                 cb.failure(t) | ||||
|             } | ||||
|         }) | ||||
|         return call | ||||
|     } | ||||
| 
 | ||||
|     private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) { | ||||
|         val userName = sessionManager.userName | ||||
|         val password = sessionManager.password | ||||
|         if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) { | ||||
|             retries++ | ||||
|             logoutClient.logout() | ||||
|             login(userName, password, callback) { | ||||
|                 Timber.i("retrying...") | ||||
|                 cancel() | ||||
|                 csrfTokenCall = request(csrfTokenInterface, callback) | ||||
|             } | ||||
|         } else { | ||||
|             callback.failure(caught()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun login( | ||||
|         username: String, | ||||
|         password: String, | ||||
|         callback: Callback, | ||||
|         retryCallback: () -> Unit | ||||
|     ) = loginClient.request(username, password, object : LoginCallback { | ||||
|         override fun success(loginResult: LoginResult) { | ||||
|             if (loginResult.pass) { | ||||
|                 sessionManager.updateAccount(loginResult) | ||||
|                 retryCallback() | ||||
|             } else { | ||||
|                 callback.failure(LoginFailedException(loginResult.message)) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         override fun twoFactorPrompt(caught: Throwable, token: String?) = | ||||
|             callback.twoFactorPrompt() | ||||
| 
 | ||||
|         // 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 error(caught: Throwable) = callback.failure(caught) | ||||
|     }) | ||||
| 
 | ||||
|     private fun cancel() { | ||||
|         loginClient.cancel() | ||||
|         if (csrfTokenCall != null) { | ||||
|             csrfTokenCall!!.cancel() | ||||
|             csrfTokenCall = null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface Callback { | ||||
|         fun success(token: String?) | ||||
|         fun failure(caught: Throwable?) | ||||
|         fun twoFactorPrompt() | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val ANON_TOKEN = "+\\" | ||||
|         private const val MAX_RETRIES = 1 | ||||
|         private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2 | ||||
|         const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure." | ||||
|         const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token." | ||||
|     } | ||||
| } | ||||
| class InvalidLoginTokenException(message: String) : Exception(message) | ||||
| 
 | ||||
|  | @ -0,0 +1,13 @@ | |||
| package fr.free.nrw.commons.auth.csrf | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import retrofit2.Call | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Headers | ||||
| 
 | ||||
| interface CsrfTokenInterface { | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf") | ||||
|     fun getCsrfTokenCall(): Call<MwQueryResponse?> | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package fr.free.nrw.commons.auth.csrf | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) { | ||||
|     fun logout() = store.clear() | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| interface LoginCallback { | ||||
|     fun success(loginResult: LoginResult) | ||||
|     fun twoFactorPrompt(caught: Throwable, token: String?) | ||||
|     fun passwordResetPrompt(token: String?) | ||||
|     fun error(caught: Throwable) | ||||
| } | ||||
							
								
								
									
										193
									
								
								app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,193 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| import android.text.TextUtils | ||||
| import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult | ||||
| import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import retrofit2.Call | ||||
| import retrofit2.Callback | ||||
| import retrofit2.Response | ||||
| import timber.log.Timber | ||||
| import java.io.IOException | ||||
| 
 | ||||
| /** | ||||
|  * Responsible for making login related requests to the server. | ||||
|  */ | ||||
| class LoginClient(private val loginInterface: LoginInterface) { | ||||
|     private var tokenCall: Call<MwQueryResponse?>? = null | ||||
|     private var loginCall: Call<LoginResponse?>? = null | ||||
| 
 | ||||
|     /** | ||||
|      * userLanguage | ||||
|      * It holds the value of the user's device language code. | ||||
|      * For example, if user's device language is English it will hold En | ||||
|      * The value will be fetched when the user clicks Login Button in the LoginActivity | ||||
|      */ | ||||
|     private var userLanguage = "" | ||||
| 
 | ||||
|     private fun getLoginToken() = loginInterface.getLoginToken() | ||||
| 
 | ||||
|     fun request(userName: String, password: String, cb: LoginCallback) { | ||||
|         cancel() | ||||
| 
 | ||||
|         tokenCall = getLoginToken() | ||||
|         tokenCall!!.enqueue(object : Callback<MwQueryResponse?> { | ||||
|             override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) { | ||||
|                 login( | ||||
|                     userName, password, null, null, response.body()!!.query()!!.loginToken(), | ||||
|                     userLanguage, cb | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) { | ||||
|                 if (call.isCanceled) { | ||||
|                     return | ||||
|                 } | ||||
|                 cb.error(caught) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fun login( | ||||
|         userName: String, password: String, retypedPassword: String?, twoFactorCode: String?, | ||||
|         loginToken: String?, userLanguage: String, cb: LoginCallback | ||||
|     ) { | ||||
|         this.userLanguage = userLanguage | ||||
| 
 | ||||
|         loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { | ||||
|             loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) | ||||
|         } else { | ||||
|             loginInterface.postLogIn( | ||||
|                 userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         loginCall!!.enqueue(object : Callback<LoginResponse?> { | ||||
|             override fun onResponse( | ||||
|                 call: Call<LoginResponse?>, | ||||
|                 response: Response<LoginResponse?> | ||||
|             ) { | ||||
|                 val loginResult = response.body()?.toLoginResult(password) | ||||
|                 if (loginResult != null) { | ||||
|                     if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) { | ||||
|                         // The server could do some transformations on user names, e.g. on some | ||||
|                         // wikis is uppercases the first letter. | ||||
|                         getExtendedInfo(loginResult.userName, loginResult, cb) | ||||
|                     } else if ("UI" == loginResult.status) { | ||||
|                         when (loginResult) { | ||||
|                             is OAuthResult -> cb.twoFactorPrompt( | ||||
|                                 LoginFailedException(loginResult.message), | ||||
|                                 loginToken | ||||
|                             ) | ||||
| 
 | ||||
|                             is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) | ||||
| 
 | ||||
|                             is LoginResult.Result -> cb.error( | ||||
|                                 LoginFailedException(loginResult.message) | ||||
|                             ) | ||||
|                         } | ||||
|                     } else { | ||||
|                         cb.error(LoginFailedException(loginResult.message)) | ||||
|                     } | ||||
|                 } else { | ||||
|                     cb.error(IOException("Login failed. Unexpected response.")) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(call: Call<LoginResponse?>, t: Throwable) { | ||||
|                 if (call.isCanceled) { | ||||
|                     return | ||||
|                 } | ||||
|                 cb.error(t) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fun doLogin( | ||||
|         username: String, | ||||
|         password: String, | ||||
|         twoFactorCode: String, | ||||
|         userLanguage: String, | ||||
|         loginCallback: LoginCallback | ||||
|     ) { | ||||
|         getLoginToken().enqueue(object :Callback<MwQueryResponse?>{ | ||||
|             override fun onResponse( | ||||
|                 call: Call<MwQueryResponse?>, | ||||
|                 response: Response<MwQueryResponse?> | ||||
|             ) = if (response.isSuccessful){ | ||||
|                 val loginToken = response.body()?.query()?.loginToken() | ||||
|                 loginToken?.let { | ||||
|                     login(username, password, null, twoFactorCode, it, userLanguage, loginCallback) | ||||
|                 } ?: run { | ||||
|                     loginCallback.error(IOException("Failed to retrieve login token")) | ||||
|                 } | ||||
|             } else { | ||||
|                 loginCallback.error(IOException("Failed to retrieve login token")) | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) { | ||||
|                 loginCallback.error(t) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|     @Throws(Throwable::class) | ||||
|     fun loginBlocking(userName: String, password: String, twoFactorCode: String?) { | ||||
|         val tokenResponse = getLoginToken().execute() | ||||
|         if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) { | ||||
|             throw IOException("Unexpected response when getting login token.") | ||||
|         } | ||||
| 
 | ||||
|         val loginToken = tokenResponse.body()?.query()?.loginToken() | ||||
|         val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) { | ||||
|             loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) | ||||
|         } else { | ||||
|             loginInterface.postLogIn( | ||||
|                 userName, password, null, twoFactorCode, loginToken, userLanguage, true | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         val response = tempLoginCall.execute() | ||||
|         val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.") | ||||
|         val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") | ||||
| 
 | ||||
|         if ("UI" == loginResult.status) { | ||||
|             if (loginResult is OAuthResult) { | ||||
|                 // TODO: Find a better way to boil up the warning about 2FA | ||||
|                 throw LoginFailedException(loginResult.message) | ||||
|             } | ||||
|             throw LoginFailedException(loginResult.message) | ||||
|         } | ||||
| 
 | ||||
|         if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) { | ||||
|             throw LoginFailedException(loginResult.message) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) = | ||||
|         loginInterface.getUserInfo(userName) | ||||
|             .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe({ response: MwQueryResponse? -> | ||||
|                 loginResult.userId = response?.query()?.userInfo()?.id() ?: 0 | ||||
|                 loginResult.groups = | ||||
|                     response?.query()?.getUserResponse(userName)?.groups ?: emptySet() | ||||
|                 cb.success(loginResult) | ||||
|             }, { caught: Throwable -> | ||||
|                 Timber.e(caught, "Login succeeded but getting group information failed. ") | ||||
|                 cb.error(caught) | ||||
|             }) | ||||
| 
 | ||||
|     fun cancel() { | ||||
|         tokenCall?.let { | ||||
|             it.cancel() | ||||
|             tokenCall = null | ||||
|         } | ||||
| 
 | ||||
|         loginCall?.let { | ||||
|             it.cancel() | ||||
|             loginCall = null | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| class LoginFailedException(message: String?) : Throwable(message) | ||||
|  | @ -0,0 +1,45 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX | ||||
| import io.reactivex.Observable | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import retrofit2.Call | ||||
| import retrofit2.http.Field | ||||
| import retrofit2.http.FormUrlEncoded | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Headers | ||||
| import retrofit2.http.POST | ||||
| import retrofit2.http.Query | ||||
| 
 | ||||
| interface LoginInterface { | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @GET(MW_API_PREFIX + "action=query&meta=tokens&type=login") | ||||
|     fun getLoginToken(): Call<MwQueryResponse?> | ||||
| 
 | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @FormUrlEncoded | ||||
|     @POST(MW_API_PREFIX + "action=clientlogin&rememberMe=") | ||||
|     fun postLogIn( | ||||
|         @Field("username") user: String?, | ||||
|         @Field("password") pass: String?, | ||||
|         @Field("logintoken") token: String?, | ||||
|         @Field("uselang") userLanguage: String?, | ||||
|         @Field("loginreturnurl") url: String? | ||||
|     ): Call<LoginResponse?> | ||||
| 
 | ||||
|     @Headers("Cache-Control: no-cache") | ||||
|     @FormUrlEncoded | ||||
|     @POST(MW_API_PREFIX + "action=clientlogin&rememberMe=") | ||||
|     fun postLogIn( | ||||
|         @Field("username") user: String?, | ||||
|         @Field("password") pass: String?, | ||||
|         @Field("retype") retypedPass: String?, | ||||
|         @Field("OATHToken") twoFactorCode: String?, | ||||
|         @Field("logintoken") token: String?, | ||||
|         @Field("uselang") userLanguage: String?, | ||||
|         @Field("logincontinue") loginContinue: Boolean | ||||
|     ): Call<LoginResponse?> | ||||
| 
 | ||||
|     @GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate") | ||||
|     fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?> | ||||
| } | ||||
|  | @ -0,0 +1,63 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName | ||||
| 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.Result | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | ||||
| 
 | ||||
| class LoginResponse { | ||||
|     @SerializedName("error") | ||||
|     val error: MwServiceError? = null | ||||
| 
 | ||||
|     @SerializedName("clientlogin") | ||||
|     private val clientLogin: ClientLogin? = null | ||||
| 
 | ||||
|     fun toLoginResult(password: String): LoginResult? { | ||||
|         return clientLogin?.toLoginResult(password) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal class ClientLogin { | ||||
|     private val status: String? = null | ||||
|     private val requests: List<Request>? = null | ||||
|     private val message: String? = null | ||||
| 
 | ||||
|     @SerializedName("username") | ||||
|     private val userName: String? = null | ||||
| 
 | ||||
|     fun toLoginResult(password: String): LoginResult { | ||||
|         var userMessage = message | ||||
|         if ("UI" == status) { | ||||
|             if (requests != null) { | ||||
|                 for (req in requests) { | ||||
|                     if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) { | ||||
|                         return OAuthResult(status, userName, password, message) | ||||
|                     } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) { | ||||
|                         return ResetPasswordResult(status, userName, password, message) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else if ("PASS" != status && "FAIL" != status) { | ||||
|             //TODO: String resource -- Looks like needed for others in this class too | ||||
|             userMessage = "An unknown error occurred." | ||||
|         } | ||||
|         return Result(status ?: "", userName, password, userMessage) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal class Request { | ||||
|     private val id: String? = null | ||||
|     private val required: String? = null | ||||
|     private val provider: String? = null | ||||
|     private val account: String? = null | ||||
|     private val fields: Map<String, RequestField>? = null | ||||
| 
 | ||||
|     fun id(): String? = id | ||||
| } | ||||
| 
 | ||||
| internal class RequestField { | ||||
|     private val type: String? = null | ||||
|     private val label: String? = null | ||||
|     private val help: String? = null | ||||
| } | ||||
|  | @ -0,0 +1,33 @@ | |||
| package fr.free.nrw.commons.auth.login | ||||
| 
 | ||||
| sealed class LoginResult( | ||||
|     val status: String, | ||||
|     val userName: String?, | ||||
|     val password: String?, | ||||
|     val message: String? | ||||
| ) { | ||||
|     var userId = 0 | ||||
|     var groups = emptySet<String>() | ||||
|     val pass: Boolean get() = "PASS" == status | ||||
| 
 | ||||
|     class Result( | ||||
|         status: String, | ||||
|         userName: String?, | ||||
|         password: String?, | ||||
|         message: String? | ||||
|     ): LoginResult(status, userName, password, message) | ||||
| 
 | ||||
|     class OAuthResult( | ||||
|         status: String, | ||||
|         userName: String?, | ||||
|         password: String?, | ||||
|         message: String? | ||||
|     ) : LoginResult(status, userName, password, message) | ||||
| 
 | ||||
|     class ResetPasswordResult( | ||||
|         status: String, | ||||
|         userName: String?, | ||||
|         password: String?, | ||||
|         message: String? | ||||
|     ) : LoginResult(status, userName, password, message) | ||||
| } | ||||
|  | @ -5,23 +5,15 @@ import android.view.LayoutInflater; | |||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| 
 | ||||
| import android.widget.FrameLayout; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| 
 | ||||
| import com.google.android.material.tabs.TabLayout; | ||||
| 
 | ||||
| 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.explore.ParentViewPager; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.contributions.ContributionController; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
|  | @ -29,12 +21,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
| 
 | ||||
|     private FragmentManager supportFragmentManager; | ||||
|     private BookmarksPagerAdapter adapter; | ||||
|     @BindView(R.id.viewPagerBookmarks) | ||||
|     ParentViewPager viewPager; | ||||
|     @BindView(R.id.tab_layout) | ||||
|     TabLayout tabLayout; | ||||
|     @BindView(R.id.fragmentContainer) | ||||
|     FrameLayout fragmentContainer; | ||||
|     FragmentBookmarksBinding binding; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionController controller; | ||||
|  | @ -54,7 +41,9 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
|     } | ||||
| 
 | ||||
|     public void setScroll(boolean canScroll) { | ||||
|         viewPager.setCanScroll(canScroll); | ||||
|         if (binding!=null) { | ||||
|             binding.viewPagerBookmarks.setCanScroll(canScroll); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -68,8 +57,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
|         @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreateView(inflater, container, savedInstanceState); | ||||
|         View view = inflater.inflate(R.layout.fragment_bookmarks, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         binding = FragmentBookmarksBinding.inflate(inflater, container, false); | ||||
| 
 | ||||
|         // Activity can call methods in the fragment by acquiring a | ||||
|         // reference to the Fragment from FragmentManager, using findFragmentById() | ||||
|  | @ -77,14 +65,14 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
| 
 | ||||
|         adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), | ||||
|             applicationKvStore.getBoolean("login_skipped")); | ||||
|         viewPager.setAdapter(adapter); | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         binding.viewPagerBookmarks.setAdapter(adapter); | ||||
|         binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks); | ||||
| 
 | ||||
|         ((MainActivity) getActivity()).showTabs(); | ||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||
| 
 | ||||
|         setupTabLayout(); | ||||
|         return view; | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -92,15 +80,15 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
|      * visibility of tabLayout to gone. | ||||
|      */ | ||||
|     public void setupTabLayout() { | ||||
|         tabLayout.setVisibility(View.VISIBLE); | ||||
|         binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|         if (adapter.getCount() == 1) { | ||||
|             tabLayout.setVisibility(View.GONE); | ||||
|             binding.tabLayout.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public void onBackPressed() { | ||||
|         if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition()))) | ||||
|         if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition()))) | ||||
|             .backPressed()) { | ||||
|             // The event is handled internally by the adapter , no further action required. | ||||
|             return; | ||||
|  | @ -108,4 +96,10 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | |||
|         // 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; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -12,8 +12,6 @@ import androidx.annotation.NonNull; | |||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; | ||||
|  | @ -22,6 +20,7 @@ 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; | ||||
|  | @ -39,8 +38,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
|     public Fragment listFragment; | ||||
|     private BookmarksPagerAdapter bookmarksPagerAdapter; | ||||
| 
 | ||||
|     @BindView(R.id.explore_container) | ||||
|     FrameLayout container; | ||||
|     FragmentFeaturedRootBinding binding; | ||||
| 
 | ||||
|     public BookmarkListRootFragment() { | ||||
|         //empty constructor necessary otherwise crashes on recreate | ||||
|  | @ -70,9 +68,8 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
|         @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         return view; | ||||
|         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -184,7 +181,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
|     public void refreshNominatedMedia(int index) { | ||||
|         if (mediaDetails != null && !listFragment.isVisible()) { | ||||
|             removeFragment(mediaDetails); | ||||
|             mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             ((BookmarkFragment) getParentFragment()).setScroll(false); | ||||
|             setFragment(mediaDetails, listFragment); | ||||
|             mediaDetails.showImage(index); | ||||
|  | @ -241,9 +238,9 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
|     @Override | ||||
|     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | ||||
|         Log.d("deneme8", "on media clicked"); | ||||
|         container.setVisibility(View.VISIBLE); | ||||
|         ((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|         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); | ||||
|  | @ -253,4 +250,10 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
|     public void onBackStackChanged() { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import androidx.fragment.app.FragmentPagerAdapter; | |||
| import java.util.ArrayList; | ||||
| 
 | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; | ||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; | ||||
| 
 | ||||
| public class BookmarksPagerAdapter extends FragmentPagerAdapter { | ||||
|  |  | |||
|  | @ -12,10 +12,9 @@ import androidx.annotation.NonNull; | |||
| import androidx.annotation.Nullable; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| 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; | ||||
|  | @ -26,17 +25,7 @@ import org.jetbrains.annotations.NotNull; | |||
|  */ | ||||
| public class BookmarkItemsFragment extends DaggerFragment { | ||||
| 
 | ||||
|     @BindView(R.id.status_message) | ||||
|     TextView statusTextView; | ||||
| 
 | ||||
|     @BindView(R.id.loading_images_progress_bar) | ||||
|     ProgressBar progressBar; | ||||
| 
 | ||||
|     @BindView(R.id.list_view) | ||||
|     RecyclerView recyclerView; | ||||
| 
 | ||||
|     @BindView(R.id.parent_layout) | ||||
|     RelativeLayout parentLayout; | ||||
|     private FragmentBookmarksItemsBinding binding; | ||||
| 
 | ||||
|     @Inject | ||||
|     BookmarkItemsController controller; | ||||
|  | @ -51,16 +40,13 @@ public class BookmarkItemsFragment extends DaggerFragment { | |||
|         final ViewGroup container, | ||||
|         final Bundle savedInstanceState | ||||
|     ) { | ||||
|         final View v = inflater.inflate(R.layout.fragment_bookmarks_items, container, false); | ||||
|         ButterKnife.bind(this, v); | ||||
|         return v; | ||||
|         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); | ||||
|         progressBar.setVisibility(View.VISIBLE); | ||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         initList(requireContext()); | ||||
|     } | ||||
| 
 | ||||
|  | @ -77,13 +63,19 @@ public class BookmarkItemsFragment extends DaggerFragment { | |||
|     private void initList(final Context context) { | ||||
|         final List<DepictedItem> depictItems = controller.loadFavoritesItems(); | ||||
|         final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context); | ||||
|         recyclerView.setAdapter(adapter); | ||||
|         progressBar.setVisibility(View.GONE); | ||||
|         binding.listView.setAdapter(adapter); | ||||
|         binding.loadingImagesProgressBar.setVisibility(View.GONE); | ||||
|         if (depictItems.isEmpty()) { | ||||
|             statusTextView.setText(R.string.bookmark_empty); | ||||
|             statusTextView.setVisibility(View.VISIBLE); | ||||
|             binding.statusMessage.setText(R.string.bookmark_empty); | ||||
|             binding.statusMessage.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             statusTextView.setVisibility(View.GONE); | ||||
|             binding.statusMessage.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -46,11 +46,11 @@ public class BookmarkLocationsDao { | |||
|         ContentProviderClient db = clientProvider.get(); | ||||
|         try { | ||||
|             cursor = db.query( | ||||
|                     BookmarkLocationsContentProvider.BASE_URI, | ||||
|                     Table.ALL_FIELDS, | ||||
|                     null, | ||||
|                     new String[]{}, | ||||
|                     null); | ||||
|                 BookmarkLocationsContentProvider.BASE_URI, | ||||
|                 Table.ALL_FIELDS, | ||||
|                 null, | ||||
|                 new String[]{}, | ||||
|                 null); | ||||
|             while (cursor != null && cursor.moveToNext()) { | ||||
|                 items.add(fromCursor(cursor)); | ||||
|             } | ||||
|  | @ -126,11 +126,11 @@ public class BookmarkLocationsDao { | |||
|         ContentProviderClient db = clientProvider.get(); | ||||
|         try { | ||||
|             cursor = db.query( | ||||
|                     BookmarkLocationsContentProvider.BASE_URI, | ||||
|                     Table.ALL_FIELDS, | ||||
|                     Table.COLUMN_NAME + "=?", | ||||
|                     new String[]{bookmarkLocation.name}, | ||||
|                     null); | ||||
|                 BookmarkLocationsContentProvider.BASE_URI, | ||||
|                 Table.ALL_FIELDS, | ||||
|                 Table.COLUMN_NAME + "=?", | ||||
|                 new String[]{bookmarkLocation.name}, | ||||
|                 null); | ||||
|             if (cursor != null && cursor.moveToFirst()) { | ||||
|                 return true; | ||||
|             } | ||||
|  | @ -149,7 +149,7 @@ public class BookmarkLocationsDao { | |||
|     @NonNull | ||||
|     Place fromCursor(final Cursor cursor) { | ||||
|         final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), | ||||
|                 cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F); | ||||
|             cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F); | ||||
| 
 | ||||
|         final Sitelinks.Builder builder = new Sitelinks.Builder(); | ||||
|         builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK))); | ||||
|  | @ -207,40 +207,40 @@ public class BookmarkLocationsDao { | |||
| 
 | ||||
|         // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. | ||||
|         public static final String[] ALL_FIELDS = { | ||||
|                 COLUMN_NAME, | ||||
|                 COLUMN_LANGUAGE, | ||||
|                 COLUMN_DESCRIPTION, | ||||
|                 COLUMN_CATEGORY, | ||||
|                 COLUMN_LABEL_TEXT, | ||||
|                 COLUMN_LABEL_ICON, | ||||
|                 COLUMN_LAT, | ||||
|                 COLUMN_LONG, | ||||
|                 COLUMN_IMAGE_URL, | ||||
|                 COLUMN_WIKIPEDIA_LINK, | ||||
|                 COLUMN_WIKIDATA_LINK, | ||||
|                 COLUMN_COMMONS_LINK, | ||||
|                 COLUMN_PIC, | ||||
|                 COLUMN_EXISTS, | ||||
|             COLUMN_NAME, | ||||
|             COLUMN_LANGUAGE, | ||||
|             COLUMN_DESCRIPTION, | ||||
|             COLUMN_CATEGORY, | ||||
|             COLUMN_LABEL_TEXT, | ||||
|             COLUMN_LABEL_ICON, | ||||
|             COLUMN_LAT, | ||||
|             COLUMN_LONG, | ||||
|             COLUMN_IMAGE_URL, | ||||
|             COLUMN_WIKIPEDIA_LINK, | ||||
|             COLUMN_WIKIDATA_LINK, | ||||
|             COLUMN_COMMONS_LINK, | ||||
|             COLUMN_PIC, | ||||
|             COLUMN_EXISTS, | ||||
|         }; | ||||
| 
 | ||||
|         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 PRIMARY KEY," | ||||
|                 + COLUMN_LANGUAGE + " STRING," | ||||
|                 + COLUMN_DESCRIPTION + " STRING," | ||||
|                 + COLUMN_CATEGORY + " STRING," | ||||
|                 + COLUMN_LABEL_TEXT + " STRING," | ||||
|                 + COLUMN_LABEL_ICON + " INTEGER," | ||||
|                 + COLUMN_LAT + " DOUBLE," | ||||
|                 + COLUMN_LONG + " DOUBLE," | ||||
|                 + COLUMN_IMAGE_URL + " STRING," | ||||
|                 + COLUMN_WIKIPEDIA_LINK + " STRING," | ||||
|                 + COLUMN_WIKIDATA_LINK + " STRING," | ||||
|                 + COLUMN_COMMONS_LINK + " STRING," | ||||
|                 + COLUMN_PIC + " STRING," | ||||
|                 + COLUMN_EXISTS + " STRING" | ||||
|                 + ");"; | ||||
|             + COLUMN_NAME + " STRING PRIMARY KEY," | ||||
|             + COLUMN_LANGUAGE + " STRING," | ||||
|             + COLUMN_DESCRIPTION + " STRING," | ||||
|             + COLUMN_CATEGORY + " STRING," | ||||
|             + COLUMN_LABEL_TEXT + " STRING," | ||||
|             + COLUMN_LABEL_ICON + " INTEGER," | ||||
|             + COLUMN_LAT + " DOUBLE," | ||||
|             + COLUMN_LONG + " DOUBLE," | ||||
|             + COLUMN_IMAGE_URL + " STRING," | ||||
|             + COLUMN_WIKIPEDIA_LINK + " STRING," | ||||
|             + COLUMN_WIKIDATA_LINK + " STRING," | ||||
|             + COLUMN_COMMONS_LINK + " STRING," | ||||
|             + COLUMN_PIC + " STRING," | ||||
|             + COLUMN_EXISTS + " STRING" | ||||
|             + ");"; | ||||
| 
 | ||||
|         public static void onCreate(SQLiteDatabase db) { | ||||
|             db.execSQL(CREATE_TABLE_STATEMENT); | ||||
|  |  | |||
|  | @ -1,41 +1,57 @@ | |||
| package fr.free.nrw.commons.bookmarks.locations; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.content.Intent; | ||||
| 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.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import dagger.android.support.DaggerFragment; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.contributions.ContributionController; | ||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions; | ||||
| import fr.free.nrw.commons.nearby.fragments.PlaceAdapter; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import kotlin.Unit; | ||||
| 
 | ||||
| public class BookmarkLocationsFragment extends DaggerFragment { | ||||
| 
 | ||||
|     @BindView(R.id.statusMessage) TextView statusTextView; | ||||
|     @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; | ||||
|     @BindView(R.id.listView) RecyclerView recyclerView; | ||||
|     @BindView(R.id.parentLayout) RelativeLayout parentLayout; | ||||
|     public FragmentBookmarksLocationsBinding binding; | ||||
| 
 | ||||
|     @Inject BookmarkLocationsController controller; | ||||
|     @Inject ContributionController contributionController; | ||||
|     @Inject BookmarkLocationsDao bookmarkLocationDao; | ||||
|     @Inject CommonPlaceClickActions commonPlaceClickActions; | ||||
|     private PlaceAdapter adapter; | ||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|         @Override | ||||
|         public void onActivityResult(Map<String, Boolean> result) { | ||||
|             boolean areAllGranted = true; | ||||
|             for(final boolean b : result.values()) { | ||||
|                 areAllGranted = areAllGranted && b; | ||||
|             } | ||||
| 
 | ||||
|             if (areAllGranted) { | ||||
|                 contributionController.locationPermissionCallback.onLocationPermissionGranted(); | ||||
|             } else { | ||||
|                 if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                     contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher); | ||||
|                 } else { | ||||
|                     contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     /** | ||||
|      * Create an instance of the fragment with the right bundle parameters | ||||
|  | @ -51,25 +67,25 @@ public class BookmarkLocationsFragment extends DaggerFragment { | |||
|             ViewGroup container, | ||||
|             Bundle savedInstanceState | ||||
|     ) { | ||||
|         View v = inflater.inflate(R.layout.fragment_bookmarks_locations, container, false); | ||||
|         ButterKnife.bind(this, v); | ||||
|         return v; | ||||
|         binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         progressBar.setVisibility(View.VISIBLE); | ||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         binding.loadingImagesProgressBar.setVisibility(View.VISIBLE); | ||||
|         binding.listView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         adapter = new PlaceAdapter(bookmarkLocationDao, | ||||
|             place -> Unit.INSTANCE, | ||||
|             (place, isBookmarked) -> { | ||||
|                 adapter.remove(place); | ||||
|                 return Unit.INSTANCE; | ||||
|             }, | ||||
|             commonPlaceClickActions | ||||
|             commonPlaceClickActions, | ||||
|             inAppCameraLocationPermissionLauncher | ||||
|         ); | ||||
|         recyclerView.setAdapter(adapter); | ||||
|         binding.listView.setAdapter(adapter); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -84,12 +100,12 @@ public class BookmarkLocationsFragment extends DaggerFragment { | |||
|     private void initList() { | ||||
|         List<Place> places = controller.loadFavoritesLocations(); | ||||
|         adapter.setItems(places); | ||||
|         progressBar.setVisibility(View.GONE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(View.GONE); | ||||
|         if (places.size() <= 0) { | ||||
|             statusTextView.setText(R.string.bookmark_empty); | ||||
|             statusTextView.setVisibility(View.VISIBLE); | ||||
|             binding.statusMessage.setText(R.string.bookmark_empty); | ||||
|             binding.statusMessage.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             statusTextView.setVisibility(View.GONE); | ||||
|             binding.statusMessage.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -97,4 +113,10 @@ public class BookmarkLocationsFragment extends DaggerFragment { | |||
|     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -9,20 +9,15 @@ import android.view.LayoutInflater; | |||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.AdapterView; | ||||
| import android.widget.GridView; | ||||
| import android.widget.ListAdapter; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.TextView; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| 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; | ||||
|  | @ -37,11 +32,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|     private GridViewAdapter gridAdapter; | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|     @BindView(R.id.statusMessage) TextView statusTextView; | ||||
|     @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; | ||||
|     @BindView(R.id.bookmarkedPicturesList) GridView gridView; | ||||
|     @BindView(R.id.parentLayout) RelativeLayout parentLayout; | ||||
| 
 | ||||
|     private FragmentBookmarksPicturesBinding binding; | ||||
|     @Inject | ||||
|     BookmarkPicturesController controller; | ||||
| 
 | ||||
|  | @ -59,15 +50,14 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|             ViewGroup container, | ||||
|             Bundle savedInstanceState | ||||
|     ) { | ||||
|         View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false); | ||||
|         ButterKnife.bind(this, v); | ||||
|         return v; | ||||
|         binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); | ||||
|         binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); | ||||
|         initList(); | ||||
|     } | ||||
| 
 | ||||
|  | @ -81,13 +71,14 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         compositeDisposable.clear(); | ||||
|         binding = null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         if (controller.needRefreshBookmarkedPictures()) { | ||||
|             gridView.setVisibility(GONE); | ||||
|             binding.bookmarkedPicturesList.setVisibility(GONE); | ||||
|             if (gridAdapter != null) { | ||||
|                 gridAdapter.clear(); | ||||
|                 ((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged(); | ||||
|  | @ -107,8 +98,8 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         progressBar.setVisibility(VISIBLE); | ||||
|         statusTextView.setVisibility(GONE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(VISIBLE); | ||||
|         binding.statusMessage.setVisibility(GONE); | ||||
| 
 | ||||
|         compositeDisposable.add(controller.loadBookmarkedPictures() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|  | @ -120,12 +111,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|      * Handles the UI updates for no internet scenario | ||||
|      */ | ||||
|     private void handleNoInternet() { | ||||
|         progressBar.setVisibility(GONE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||
|             statusTextView.setVisibility(VISIBLE); | ||||
|             statusTextView.setText(getString(R.string.no_internet)); | ||||
|             binding.statusMessage.setVisibility(VISIBLE); | ||||
|             binding.statusMessage.setText(getString(R.string.no_internet)); | ||||
|         } else { | ||||
|             ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet); | ||||
|             ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -136,7 +127,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|     private void handleError(Throwable throwable) { | ||||
|         Timber.e(throwable, "Error occurred while loading images inside a category"); | ||||
|         try{ | ||||
|             ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images); | ||||
|             ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images); | ||||
|             initErrorView(); | ||||
|         }catch (Exception e){ | ||||
|             e.printStackTrace(); | ||||
|  | @ -147,12 +138,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|      * Handles the UI updates for a error scenario | ||||
|      */ | ||||
|     private void initErrorView() { | ||||
|         progressBar.setVisibility(GONE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||
|             statusTextView.setVisibility(VISIBLE); | ||||
|             statusTextView.setText(getString(R.string.no_images_found)); | ||||
|             binding.statusMessage.setVisibility(VISIBLE); | ||||
|             binding.statusMessage.setText(getString(R.string.no_images_found)); | ||||
|         } else { | ||||
|             statusTextView.setVisibility(GONE); | ||||
|             binding.statusMessage.setVisibility(GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -160,12 +151,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|      * Handles the UI updates when there is no bookmarks | ||||
|      */ | ||||
|     private void initEmptyBookmarkListView() { | ||||
|         progressBar.setVisibility(GONE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||
|             statusTextView.setVisibility(VISIBLE); | ||||
|             statusTextView.setText(getString(R.string.bookmark_empty)); | ||||
|             binding.statusMessage.setVisibility(VISIBLE); | ||||
|             binding.statusMessage.setText(getString(R.string.bookmark_empty)); | ||||
|         } else { | ||||
|             statusTextView.setVisibility(GONE); | ||||
|             binding.statusMessage.setVisibility(GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -188,18 +179,18 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|             setAdapter(collection); | ||||
|         } else { | ||||
|             if (gridAdapter.containsAll(collection)) { | ||||
|                 progressBar.setVisibility(GONE); | ||||
|                 statusTextView.setVisibility(GONE); | ||||
|                 gridView.setVisibility(VISIBLE); | ||||
|                 gridView.setAdapter(gridAdapter); | ||||
|                 binding.loadingImagesProgressBar.setVisibility(GONE); | ||||
|                 binding.statusMessage.setVisibility(GONE); | ||||
|                 binding.bookmarkedPicturesList.setVisibility(VISIBLE); | ||||
|                 binding.bookmarkedPicturesList.setAdapter(gridAdapter); | ||||
|                 return; | ||||
|             } | ||||
|             gridAdapter.addItems(collection); | ||||
|             ((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged(); | ||||
|         } | ||||
|         progressBar.setVisibility(GONE); | ||||
|         statusTextView.setVisibility(GONE); | ||||
|         gridView.setVisibility(VISIBLE); | ||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||
|         binding.statusMessage.setVisibility(GONE); | ||||
|         binding.bookmarkedPicturesList.setVisibility(VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -212,7 +203,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|                 R.layout.layout_category_images, | ||||
|                 mediaList | ||||
|         ); | ||||
|         gridView.setAdapter(gridAdapter); | ||||
|         binding.bookmarkedPicturesList.setAdapter(gridAdapter); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -221,6 +212,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|      * @return  GridView Adapter | ||||
|      */ | ||||
|     public ListAdapter getAdapter() { | ||||
|         return gridView.getAdapter(); | ||||
|         return binding.bookmarkedPicturesList.getAdapter(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -3,22 +3,20 @@ package fr.free.nrw.commons.campaigns; | |||
| import android.content.Context; | ||||
| import android.net.Uri; | ||||
| import android.util.AttributeSet; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import fr.free.nrw.commons.campaigns.models.Campaign; | ||||
| import fr.free.nrw.commons.databinding.LayoutCampaginBinding; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import org.wikipedia.util.DateUtil; | ||||
| import fr.free.nrw.commons.utils.DateUtil; | ||||
| 
 | ||||
| import java.text.ParseException; | ||||
| import java.util.Date; | ||||
| 
 | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.contributions.MainActivity; | ||||
|  | @ -31,6 +29,7 @@ import fr.free.nrw.commons.utils.ViewUtil; | |||
|  */ | ||||
| public class CampaignView extends SwipableCardView { | ||||
|     Campaign campaign; | ||||
|     private LayoutCampaginBinding binding; | ||||
|     private ViewHolder viewHolder; | ||||
| 
 | ||||
|     public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView"; | ||||
|  | @ -69,15 +68,15 @@ public class CampaignView extends SwipableCardView { | |||
|     @Override public boolean onSwipe(final View view) { | ||||
|         view.setVisibility(View.GONE); | ||||
|         ((BaseActivity) getContext()).defaultKvStore | ||||
|             .putBoolean(campaignPreference, false); | ||||
|             .putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false); | ||||
|         ViewUtil.showLongToast(getContext(), | ||||
|             getResources().getString(R.string.nearby_campaign_dismiss_message)); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private void init() { | ||||
|         final View rootView = inflate(getContext(), R.layout.layout_campagin, this); | ||||
|         viewHolder = new ViewHolder(rootView); | ||||
|         binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true); | ||||
|         viewHolder = new ViewHolder(); | ||||
|         setOnClickListener(view -> { | ||||
|             if (campaign != null) { | ||||
|                 if (campaign.isWLMCampaign()) { | ||||
|  | @ -90,27 +89,16 @@ public class CampaignView extends SwipableCardView { | |||
|     } | ||||
| 
 | ||||
|     public class ViewHolder { | ||||
| 
 | ||||
|         @BindView(R.id.iv_campaign) | ||||
|         ImageView ivCampaign; | ||||
|         @BindView(R.id.tv_title) TextView tvTitle; | ||||
|         @BindView(R.id.tv_description) TextView tvDescription; | ||||
|         @BindView(R.id.tv_dates) TextView tvDates; | ||||
| 
 | ||||
|         public ViewHolder(View itemView) { | ||||
|             ButterKnife.bind(this, itemView); | ||||
|         } | ||||
| 
 | ||||
|         public void init() { | ||||
|             if (campaign != null) { | ||||
|                 ivCampaign.setImageDrawable( | ||||
|                 binding.ivCampaign.setImageDrawable( | ||||
|                     getResources().getDrawable(R.drawable.ic_campaign)); | ||||
| 
 | ||||
|                 tvTitle.setText(campaign.getTitle()); | ||||
|                 tvDescription.setText(campaign.getDescription()); | ||||
|                 binding.tvTitle.setText(campaign.getTitle()); | ||||
|                 binding.tvDescription.setText(campaign.getDescription()); | ||||
|                 try { | ||||
|                     if (campaign.isWLMCampaign()) { | ||||
|                         tvDates.setText( | ||||
|                         binding.tvDates.setText( | ||||
|                             String.format("%1s - %2s", campaign.getStartDate(), | ||||
|                                 campaign.getEndDate())); | ||||
|                     } else { | ||||
|  | @ -118,7 +106,7 @@ public class CampaignView extends SwipableCardView { | |||
|                             .parse(campaign.getStartDate()); | ||||
|                         final Date endDate = CommonsDateUtil.getIso8601DateFormatShort() | ||||
|                             .parse(campaign.getEndDate()); | ||||
|                         tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate), | ||||
|                         binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate), | ||||
|                             DateUtil.getExtraShortDateString(endDate))); | ||||
|                     } | ||||
|                 } catch (final ParseException e) { | ||||
|  |  | |||
|  | @ -27,30 +27,42 @@ class CategoriesModel @Inject constructor( | |||
|     private var selectedExistingCategories: MutableList<String> = mutableListOf() | ||||
| 
 | ||||
|     /** | ||||
|      * Returns if the item contains an year | ||||
|      * @param item | ||||
|      * Returns true if an item is considered to be a spammy category which should be ignored | ||||
|      * | ||||
|      * @param item a category item that needs to be validated to know if it is spammy or not | ||||
|      * @return | ||||
|      */ | ||||
|     fun containsYear(item: String): Boolean { | ||||
|     fun isSpammyCategory(item: String): Boolean { | ||||
|         //Check for current and previous year to exclude these categories from removal | ||||
|         val now = Calendar.getInstance() | ||||
|         val year = now[Calendar.YEAR] | ||||
|         val yearInString = year.toString() | ||||
|         val prevYear = year - 1 | ||||
|         val curYear = now[Calendar.YEAR] | ||||
|         val curYearInString = curYear.toString() | ||||
|         val prevYear = curYear - 1 | ||||
|         val prevYearInString = prevYear.toString() | ||||
|         Timber.d("Previous year: %s", prevYearInString) | ||||
| 
 | ||||
|         //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) | ||||
|         //And that item does not equal the current year or previous year | ||||
|         //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) | ||||
|         //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 | ||||
|         return item.matches(".*(19|20)\\d{2}.*".toRegex()) | ||||
|                 && !item.contains(yearInString) | ||||
|                 && !item.contains(prevYearInString) | ||||
|                 || item.matches("(.*)needing(.*)".toRegex()) | ||||
|                 || item.matches("(.*)taken on(.*)".toRegex()) | ||||
|                 || item.matches(".*0s.*".toRegex()) | ||||
|                 && !item.matches(".*(200|201)0s.*".toRegex()) | ||||
|         val mentionsDecade = item.matches(".*0s.*".toRegex()) | ||||
|         val recentDecade = item.matches(".*20[0-2]0s.*".toRegex()) | ||||
|         val spammyCategory = item.matches("(.*)needing(.*)".toRegex()) | ||||
|                           || item.matches("(.*)taken on(.*)".toRegex()) | ||||
| 
 | ||||
|         // always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750) | ||||
|         if (spammyCategory) { | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         if (mentionsDecade) { | ||||
|             // Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029 | ||||
|             // Example: "2020s" is OK, but "1920s" is not (and should be skipped) | ||||
|             return !recentDecade | ||||
|         } else { | ||||
|             // If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year | ||||
|             // anywhere within the string (.* is wildcard) (Issue #47) | ||||
|             // And that item does not equal the current year or previous year | ||||
|             return item.matches(".*(19|20)\\d{2}.*".toRegex()) | ||||
|                     && !item.contains(curYearInString) | ||||
|                     && !item.contains(prevYearInString) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -136,7 +148,11 @@ class CategoriesModel @Inject constructor( | |||
|         return Observable.fromIterable(categoryNames) | ||||
|             .map { categoryName -> | ||||
|                 buildCategories(categoryName) | ||||
|             }.toList().toObservable() | ||||
|             } | ||||
|             .filter { categoryItem -> | ||||
|                 categoryItem.name != "Hidden" | ||||
|             } | ||||
|             .toList().toObservable() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| import io.reactivex.Single | ||||
| import org.wikipedia.dataclient.mwapi.MwQueryResponse | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,13 +15,12 @@ import androidx.appcompat.widget.Toolbar; | |||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import com.google.android.material.tabs.TabLayout; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.ViewPagerAdapter; | ||||
| import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding; | ||||
| 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.sub.SubCategoriesFragment; | ||||
|  | @ -29,7 +28,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment; | |||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import org.wikipedia.page.PageTitle; | ||||
| import fr.free.nrw.commons.wikidata.model.page.PageTitle; | ||||
| 
 | ||||
| /** | ||||
|  * This activity displays details of a particular category | ||||
|  | @ -45,23 +44,23 @@ public class CategoryDetailsActivity extends BaseActivity | |||
|     private CategoriesMediaFragment categoriesMediaFragment; | ||||
|     private MediaDetailPagerFragment mediaDetails; | ||||
|     private String categoryName; | ||||
|     @BindView(R.id.mediaContainer) FrameLayout mediaContainer; | ||||
|     @BindView(R.id.tab_layout) TabLayout tabLayout; | ||||
|     @BindView(R.id.viewPager) ViewPager viewPager; | ||||
|     @BindView(R.id.toolbar) Toolbar toolbar; | ||||
|     ViewPagerAdapter viewPagerAdapter; | ||||
| 
 | ||||
|     private ActivityCategoryDetailsBinding binding; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_category_details); | ||||
|         ButterKnife.bind(this); | ||||
| 
 | ||||
|         binding = ActivityCategoryDetailsBinding.inflate(getLayoutInflater()); | ||||
|         final View view = binding.getRoot(); | ||||
|         setContentView(view); | ||||
|         supportFragmentManager = getSupportFragmentManager(); | ||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||
|         viewPager.setAdapter(viewPagerAdapter); | ||||
|         viewPager.setOffscreenPageLimit(2); | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         setSupportActionBar(toolbar); | ||||
|         binding.viewPager.setAdapter(viewPagerAdapter); | ||||
|         binding.viewPager.setOffscreenPageLimit(2); | ||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||
|         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|         setTabs(); | ||||
|         setPageTitle(); | ||||
|  | @ -110,12 +109,12 @@ public class CategoryDetailsActivity extends BaseActivity | |||
|      */ | ||||
|     @Override | ||||
|     public void onMediaClicked(int position) { | ||||
|         tabLayout.setVisibility(View.GONE); | ||||
|         viewPager.setVisibility(View.GONE); | ||||
|         mediaContainer.setVisibility(View.VISIBLE); | ||||
|         binding.tabLayout.setVisibility(View.GONE); | ||||
|         binding.viewPager.setVisibility(View.GONE); | ||||
|         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||
|         if (mediaDetails == null || !mediaDetails.isVisible()) { | ||||
|             // set isFeaturedImage true for featured images, to include author field on media detail | ||||
|             mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             FragmentManager supportFragmentManager = getSupportFragmentManager(); | ||||
|             supportFragmentManager | ||||
|                     .beginTransaction() | ||||
|  | @ -216,9 +215,9 @@ public class CategoryDetailsActivity extends BaseActivity | |||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         if (supportFragmentManager.getBackStackEntryCount() == 1){ | ||||
|             tabLayout.setVisibility(View.VISIBLE); | ||||
|             viewPager.setVisibility(View.VISIBLE); | ||||
|             mediaContainer.setVisibility(View.GONE); | ||||
|             binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             binding.viewPager.setVisibility(View.VISIBLE); | ||||
|             binding.mediaContainer.setVisibility(View.GONE); | ||||
|         } | ||||
|         super.onBackPressed(); | ||||
|     } | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_E | |||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.util.Log; | ||||
| import fr.free.nrw.commons.BuildConfig; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
|  |  | |||
|  | @ -1,71 +0,0 @@ | |||
| package fr.free.nrw.commons.category; | ||||
| 
 | ||||
| import io.reactivex.Single; | ||||
| import java.util.Map; | ||||
| import org.wikipedia.dataclient.mwapi.MwQueryResponse; | ||||
| import retrofit2.http.GET; | ||||
| import retrofit2.http.Query; | ||||
| import retrofit2.http.QueryMap; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for interacting with Commons category related APIs | ||||
|  */ | ||||
| public interface CategoryInterface { | ||||
| 
 | ||||
|     /** | ||||
|      * Searches for categories with the specified name. | ||||
|      * | ||||
|      * @param filter    The string to be searched | ||||
|      * @param itemLimit How many results are returned | ||||
|      * @return | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2" | ||||
|             + "&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70" | ||||
|             + "&gsrnamespace=14") | ||||
|     Single<MwQueryResponse> searchCategories(@Query("gsrsearch") String filter, | ||||
|                                                  @Query("gsrlimit") int itemLimit, | ||||
|                                                  @Query("gsroffset") int offset); | ||||
| 
 | ||||
|     /** | ||||
|      * Searches for categories starting with the specified prefix. | ||||
|      * | ||||
|      * @param prefix    The string to be searched | ||||
|      * @param itemLimit How many results are returned | ||||
|      * @return | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2" | ||||
|             + "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail" | ||||
|             + "&pithumbsize=70") | ||||
|     Single<MwQueryResponse> searchCategoriesForPrefix(@Query("gacprefix") String prefix, | ||||
|                                                           @Query("gaclimit") int itemLimit, | ||||
|                                                           @Query("gacoffset") int offset); | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches categories starting and ending with a specified name. | ||||
|      * | ||||
|      * @param startingCategory Name of the category to start | ||||
|      * @param endingCategory Name of the category to end | ||||
|      * @param itemLimit How many categories to return | ||||
|      * @param offset offset | ||||
|      * @return MwQueryResponse | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2" | ||||
|         + "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail" | ||||
|         + "&pithumbsize=70") | ||||
|     Single<MwQueryResponse> getCategoriesByName(@Query("gacfrom") String startingCategory, | ||||
|         @Query("gacto") String endingCategory, | ||||
|         @Query("gaclimit") int itemLimit, | ||||
|         @Query("gacoffset") int offset); | ||||
| 
 | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2" | ||||
|             + "&generator=categorymembers&gcmtype=subcat" | ||||
|             + "&prop=info&gcmlimit=50") | ||||
|     Single<MwQueryResponse> getSubCategoryList(@Query("gcmtitle") String categoryName, | ||||
|         @QueryMap(encoded = true) Map<String, String> continuation); | ||||
| 
 | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2" | ||||
|             + "&generator=categories&prop=info&gcllimit=50") | ||||
|     Single<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName, | ||||
|         @QueryMap(encoded = true) Map<String, String> continuation); | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,69 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import io.reactivex.Single | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Query | ||||
| import retrofit2.http.QueryMap | ||||
| 
 | ||||
| /** | ||||
|  * Interface for interacting with Commons category related APIs | ||||
|  */ | ||||
| interface CategoryInterface { | ||||
|     /** | ||||
|      * Searches for categories with the specified name. | ||||
|      * | ||||
|      * @param filter    The string to be searched | ||||
|      * @param itemLimit How many results are returned | ||||
|      * @return | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14") | ||||
|     fun searchCategories( | ||||
|         @Query("gsrsearch") filter: String?, | ||||
|         @Query("gsrlimit") itemLimit: Int, | ||||
|         @Query("gsroffset") offset: Int | ||||
|     ): Single<MwQueryResponse> | ||||
| 
 | ||||
|     /** | ||||
|      * Searches for categories starting with the specified prefix. | ||||
|      * | ||||
|      * @param prefix    The string to be searched | ||||
|      * @param itemLimit How many results are returned | ||||
|      * @return | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70") | ||||
|     fun searchCategoriesForPrefix( | ||||
|         @Query("gacprefix") prefix: String?, | ||||
|         @Query("gaclimit") itemLimit: Int, | ||||
|         @Query("gacoffset") offset: Int | ||||
|     ): Single<MwQueryResponse> | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches categories starting and ending with a specified name. | ||||
|      * | ||||
|      * @param startingCategory Name of the category to start | ||||
|      * @param endingCategory Name of the category to end | ||||
|      * @param itemLimit How many categories to return | ||||
|      * @param offset offset | ||||
|      * @return MwQueryResponse | ||||
|      */ | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70") | ||||
|     fun getCategoriesByName( | ||||
|         @Query("gacfrom") startingCategory: String?, | ||||
|         @Query("gacto") endingCategory: String?, | ||||
|         @Query("gaclimit") itemLimit: Int, | ||||
|         @Query("gacoffset") offset: Int | ||||
|     ): Single<MwQueryResponse> | ||||
| 
 | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50") | ||||
|     fun getSubCategoryList( | ||||
|         @Query("gcmtitle") categoryName: String, | ||||
|         @QueryMap(encoded = true) continuation: Map<String, String> | ||||
|     ): Single<MwQueryResponse> | ||||
| 
 | ||||
|     @GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50") | ||||
|     fun getParentCategoryList( | ||||
|         @Query("titles") categoryName: String?, | ||||
|         @QueryMap(encoded = true) continuation: Map<String, String> | ||||
|     ): Single<MwQueryResponse> | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| import android.os.Parcelable | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| import kotlinx.parcelize.Parcelize | ||||
| 
 | ||||
| @Parcelize | ||||
| data class CategoryItem(val name: String, val description: String?, | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import android.os.Parcelable | |||
| import androidx.room.Embedded | ||||
| import androidx.room.Entity | ||||
| import androidx.room.PrimaryKey | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.upload.UploadItem | ||||
|  | @ -12,8 +13,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail | |||
| import fr.free.nrw.commons.upload.WikidataPlace | ||||
| import fr.free.nrw.commons.upload.WikidataPlace.Companion.from | ||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||
| import kotlinx.android.parcel.Parcelize | ||||
| import java.util.* | ||||
| import kotlinx.parcelize.Parcelize | ||||
| import java.io.File | ||||
| import java.util.Date | ||||
| 
 | ||||
| @Entity(tableName = "contribution") | ||||
| @Parcelize | ||||
|  | @ -43,7 +45,11 @@ data class Contribution constructor( | |||
|     var hasInvalidLocation : Int =  0, | ||||
|     var contentUri: Uri? = null, | ||||
|     var countryCode : String? = null, | ||||
|     var imageSHA1 : String? = null | ||||
|     var imageSHA1 : String? = null, | ||||
|     /** | ||||
|      * Number of times a contribution has been retried after a failure | ||||
|      */ | ||||
|     var retries: Int = 0 | ||||
| ) : Parcelable { | ||||
| 
 | ||||
|     fun completeWith(media: Media): Contribution { | ||||
|  | @ -111,6 +117,21 @@ data class Contribution constructor( | |||
|          */ | ||||
|         fun formatDescriptions(descriptions: List<UploadMediaDetail>) = | ||||
|             descriptions.filter { it.descriptionText.isNotEmpty() } | ||||
|                 .joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" } | ||||
|                 .joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" } | ||||
|     } | ||||
| 
 | ||||
|     val fileKey : String? get() = chunkInfo?.uploadResult?.filekey | ||||
|     val localUriPath: File? get() = localUri?.path?.let { File(it) } | ||||
| 
 | ||||
|     fun isCompleted(): Boolean { | ||||
|         return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload | ||||
|     } | ||||
| 
 | ||||
|     fun isPaused(): Boolean { | ||||
|         return CommonsApplication.pauseUploads[pageId] ?: false | ||||
|     } | ||||
| 
 | ||||
|     fun unpause() { | ||||
|         CommonsApplication.pauseUploads[pageId] = false | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,13 +2,12 @@ package fr.free.nrw.commons.contributions; | |||
| 
 | ||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.Manifest.permission; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Build.VERSION; | ||||
| import android.os.Build.VERSION_CODES; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.filepicker.DefaultCallback; | ||||
|  | @ -16,8 +15,13 @@ import fr.free.nrw.commons.filepicker.FilePicker; | |||
| import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.ArrayList; | ||||
|  | @ -31,6 +35,13 @@ public class ContributionController { | |||
| 
 | ||||
|     public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; | ||||
|     private final JsonKvStore defaultKvStore; | ||||
|     private LatLng locationBeforeImageCapture; | ||||
|     private boolean isInAppCameraUpload; | ||||
|     public LocationPermissionCallback locationPermissionCallback; | ||||
|     private LocationPermissionsHelper locationPermissionsHelper; | ||||
| 
 | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { | ||||
|  | @ -40,7 +51,8 @@ public class ContributionController { | |||
|     /** | ||||
|      * Check for permissions and initiate camera click | ||||
|      */ | ||||
|     public void initiateCameraPick(Activity activity) { | ||||
|     public void initiateCameraPick(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) { | ||||
|         boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||
|         if (!useExtStorage) { | ||||
|             initiateCameraUpload(activity); | ||||
|  | @ -48,10 +60,133 @@ public class ContributionController { | |||
|         } | ||||
| 
 | ||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE, | ||||
|                 () -> initiateCameraUpload(activity), | ||||
|                 R.string.storage_permission_title, | ||||
|                 R.string.write_storage_permission_rationale); | ||||
|             () -> { | ||||
|                 if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { | ||||
|                     defaultKvStore.putBoolean("inAppCameraFirstRun", false); | ||||
|                     askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher); | ||||
|                 } else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) { | ||||
|                     createDialogsAndHandleLocationPermissions(activity, | ||||
|                         inAppCameraLocationPermissionLauncher); | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity); | ||||
|                 } | ||||
|             }, | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             PermissionUtils.PERMISSIONS_STORAGE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Asks users to provide location access | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private void createDialogsAndHandleLocationPermissions(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) { | ||||
|         locationPermissionCallback = new LocationPermissionCallback() { | ||||
|             @Override | ||||
|             public void onLocationPermissionDenied(String toastMessage) { | ||||
|                 Toast.makeText( | ||||
|                     activity, | ||||
|                     toastMessage, | ||||
|                     Toast.LENGTH_LONG | ||||
|                 ).show(); | ||||
|                 initiateCameraUpload(activity); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onLocationPermissionGranted() { | ||||
|                 if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||
|                     showLocationOffDialog(activity, R.string.in_app_camera_needs_location, | ||||
|                         R.string.in_app_camera_location_unavailable); | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         locationPermissionsHelper = new LocationPermissionsHelper( | ||||
|             activity, locationManager, locationPermissionCallback); | ||||
|         if (inAppCameraLocationPermissionLauncher != null) { | ||||
|             inAppCameraLocationPermissionLauncher.launch( | ||||
|                 new String[]{permission.ACCESS_FINE_LOCATION}); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user about location services being off | ||||
|      * and asking them to turn it on | ||||
|      * TODO: Add a seperate callback in LocationPermissionsHelper for this. | ||||
|      *      Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 | ||||
|      * | ||||
|      * @param activity Activity reference | ||||
|      * @param dialogTextResource Resource id of text to be shown in dialog | ||||
|      * @param toastTextResource Resource id of text to be shown in toast | ||||
|      */ | ||||
|     private void showLocationOffDialog(Activity activity, int dialogTextResource, | ||||
|         int toastTextResource) { | ||||
|         DialogUtil | ||||
|             .showAlertDialog(activity, | ||||
|                 activity.getString(R.string.ask_to_turn_location_on), | ||||
|                 activity.getString(dialogTextResource), | ||||
|                 activity.getString(R.string.title_app_shortcut_setting), | ||||
|                 activity.getString(R.string.cancel), | ||||
|                 () -> locationPermissionsHelper.openLocationSettings(activity), | ||||
|                 () -> { | ||||
|                     Toast.makeText(activity, activity.getString(toastTextResource), | ||||
|                         Toast.LENGTH_LONG).show(); | ||||
|                     initiateCameraUpload(activity); | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     public void handleShowRationaleFlowCameraLocation(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) { | ||||
|         DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_permission_rationale), | ||||
|             activity.getString(android.R.string.ok), | ||||
|             activity.getString(android.R.string.cancel), | ||||
|             () -> { | ||||
|                 createDialogsAndHandleLocationPermissions(activity, | ||||
|                     inAppCameraLocationPermissionLauncher); | ||||
|             }, | ||||
|             () -> locationPermissionCallback.onLocationPermissionDenied( | ||||
|                 activity.getString(R.string.in_app_camera_location_permission_denied)), | ||||
|             null, | ||||
|             false); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Suggest user to attach location information with pictures. If the user selects "Yes", then: | ||||
|      * <p> | ||||
|      * Location is taken from the EXIF if the default camera application does not redact location | ||||
|      * tags. | ||||
|      * <p> | ||||
|      * Otherwise, if the EXIF metadata does not have location information, then location captured by | ||||
|      * the app is used | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private void askUserToAllowLocationAccess(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) { | ||||
|         DialogUtil.showAlertDialog(activity, | ||||
|             activity.getString(R.string.in_app_camera_location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_access_explanation), | ||||
|             activity.getString(R.string.option_allow), | ||||
|             activity.getString(R.string.option_dismiss), | ||||
|             () -> { | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", true); | ||||
|                 createDialogsAndHandleLocationPermissions(activity, | ||||
|                     inAppCameraLocationPermissionLauncher); | ||||
|             }, | ||||
|             () -> { | ||||
|                 ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied); | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", false); | ||||
|                 initiateCameraUpload(activity); | ||||
|             }, | ||||
|             null, | ||||
|             true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -65,44 +200,36 @@ public class ContributionController { | |||
|      * Initiate gallery picker with permission | ||||
|      */ | ||||
|     public void initiateCustomGalleryPickWithPermission(final Activity activity) { | ||||
|         setPickerConfiguration(activity,true); | ||||
|         setPickerConfiguration(activity, true); | ||||
| 
 | ||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||
|             Manifest.permission.WRITE_EXTERNAL_STORAGE, | ||||
|             () -> { | ||||
|                 if (VERSION.SDK_INT >= VERSION_CODES.Q) { | ||||
|                     PermissionUtils.checkPermissionsAndPerformAction( | ||||
|                         activity, | ||||
|                         permission.ACCESS_MEDIA_LOCATION, | ||||
|                         () -> {}, | ||||
|                         R.string.media_location_permission_denied, | ||||
|                         R.string.add_location_manually | ||||
|                     ); | ||||
|                 } | ||||
|                 FilePicker.openCustomSelector(activity, 0); | ||||
|             }, | ||||
|             () -> FilePicker.openCustomSelector(activity, 0), | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale); | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             PermissionUtils.PERMISSIONS_STORAGE); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Open chooser for gallery uploads | ||||
|      */ | ||||
|     private void initiateGalleryUpload(final Activity activity, final boolean allowMultipleUploads) { | ||||
|     private void initiateGalleryUpload(final Activity activity, | ||||
|         final boolean allowMultipleUploads) { | ||||
|         setPickerConfiguration(activity, allowMultipleUploads); | ||||
|         FilePicker.openGallery(activity, 0); | ||||
|         boolean openDocumentIntentPreferred = defaultKvStore.getBoolean( | ||||
|             "openDocumentPhotoPickerPref", true); | ||||
|         FilePicker.openGallery(activity, 0, openDocumentIntentPreferred); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets configuration for file picker | ||||
|      */ | ||||
|     private void setPickerConfiguration(Activity activity, | ||||
|                                         boolean allowMultipleUploads) { | ||||
|         boolean allowMultipleUploads) { | ||||
|         boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||
|         FilePicker.configuration(activity) | ||||
|                 .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) | ||||
|                 .setAllowMultiplePickInGallery(allowMultipleUploads); | ||||
|             .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) | ||||
|             .setAllowMultiplePickInGallery(allowMultipleUploads); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -110,42 +237,50 @@ public class ContributionController { | |||
|      */ | ||||
|     private void initiateCameraUpload(Activity activity) { | ||||
|         setPickerConfiguration(activity, false); | ||||
|         if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { | ||||
|             locationBeforeImageCapture = locationManager.getLastLocation(); | ||||
|         } | ||||
|         isInAppCameraUpload = true; | ||||
|         FilePicker.openCameraForImage(activity, 0); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches callback for file picker. | ||||
|      */ | ||||
|     public void handleActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { | ||||
|         FilePicker.handleActivityResult(requestCode, resultCode, data, activity, new DefaultCallback() { | ||||
|     public void handleActivityResult(Activity activity, int requestCode, int resultCode, | ||||
|         Intent data) { | ||||
|         FilePicker.handleActivityResult(requestCode, resultCode, data, activity, | ||||
|             new DefaultCallback() { | ||||
| 
 | ||||
|             @Override | ||||
|             public void onCanceled(final ImageSource source, final int type) { | ||||
|                 super.onCanceled(source, type); | ||||
|                 defaultKvStore.remove(PLACE_OBJECT); | ||||
|             } | ||||
|                 @Override | ||||
|                 public void onCanceled(final ImageSource source, final int type) { | ||||
|                     super.onCanceled(source, type); | ||||
|                     defaultKvStore.remove(PLACE_OBJECT); | ||||
|                 } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) { | ||||
|                 ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); | ||||
|             } | ||||
|                 @Override | ||||
|                 public void onImagePickerError(Exception e, FilePicker.ImageSource source, | ||||
|                     int type) { | ||||
|                     ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); | ||||
|                 } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) { | ||||
|                 Intent intent = handleImagesPicked(activity, imagesFiles); | ||||
|                 activity.startActivity(intent); | ||||
|             } | ||||
|         }); | ||||
|                 @Override | ||||
|                 public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, | ||||
|                     FilePicker.ImageSource source, int type) { | ||||
|                     Intent intent = handleImagesPicked(activity, imagesFiles); | ||||
|                     activity.startActivity(intent); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     public List<UploadableFile> handleExternalImagesPicked(Activity activity, | ||||
|                                                            Intent data) { | ||||
|         Intent data) { | ||||
|         return FilePicker.handleExternalImagesPicked(data, activity); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns intent to be passed to upload activity | ||||
|      * Attaches place object for nearby uploads | ||||
|      * Returns intent to be passed to upload activity Attaches place object for nearby uploads and | ||||
|      * location before image capture if in-app camera is used | ||||
|      */ | ||||
|     private Intent handleImagesPicked(Context context, | ||||
|         List<UploadableFile> imagesFiles) { | ||||
|  | @ -159,7 +294,17 @@ public class ContributionController { | |||
|             shareIntent.putExtra(PLACE_OBJECT, place); | ||||
|         } | ||||
| 
 | ||||
|         if (locationBeforeImageCapture != null) { | ||||
|             shareIntent.putExtra( | ||||
|                 UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE, | ||||
|                 locationBeforeImageCapture); | ||||
|         } | ||||
| 
 | ||||
|         shareIntent.putExtra( | ||||
|             UploadActivity.IN_APP_CAMERA_UPLOAD, | ||||
|             isInAppCameraUpload | ||||
|         ); | ||||
|         isInAppCameraUpload = false;    // reset the flag for next use | ||||
|         return shareIntent; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import androidx.room.Update; | |||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Single; | ||||
| import java.util.Calendar; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| 
 | ||||
| @Dao | ||||
|  |  | |||
|  | @ -12,14 +12,12 @@ import androidx.annotation.Nullable; | |||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.app.AlertDialog.Builder; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import butterknife.OnClick; | ||||
| import com.facebook.drawee.view.SimpleDraweeView; | ||||
| import com.facebook.imagepipeline.request.ImageRequest; | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | ||||
| import fr.free.nrw.commons.databinding.LayoutContributionBinding; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
|  | @ -29,29 +27,8 @@ import java.io.File; | |||
| public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||
| 
 | ||||
|     private final Callback callback; | ||||
|     @BindView(R.id.contributionImage) | ||||
|     SimpleDraweeView imageView; | ||||
|     @BindView(R.id.contributionTitle) | ||||
|     TextView titleView; | ||||
|     @BindView(R.id.authorView) | ||||
|     TextView authorView; | ||||
|     @BindView(R.id.contributionState) | ||||
|     TextView stateView; | ||||
|     @BindView(R.id.contributionSequenceNumber) | ||||
|     TextView seqNumView; | ||||
|     @BindView(R.id.contributionProgress) | ||||
|     ProgressBar progressView; | ||||
|     @BindView(R.id.image_options) | ||||
|     RelativeLayout imageOptions; | ||||
|     @BindView(R.id.wikipediaButton) | ||||
|     ImageButton addToWikipediaButton; | ||||
|     @BindView(R.id.retryButton) | ||||
|     ImageButton retryButton; | ||||
|     @BindView(R.id.cancelButton) | ||||
|     ImageButton cancelButton; | ||||
|     @BindView(R.id.pauseResumeButton) | ||||
|     ImageButton pauseResumeButton; | ||||
| 
 | ||||
|     LayoutContributionBinding binding; | ||||
| 
 | ||||
|     private int position; | ||||
|     private Contribution contribution; | ||||
|  | @ -67,9 +44,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|         super(parent); | ||||
|         this.parent = parent; | ||||
|         this.mediaClient = mediaClient; | ||||
|         ButterKnife.bind(this, parent); | ||||
|         this.callback = callback; | ||||
| 
 | ||||
|         binding = LayoutContributionBinding.bind(parent); | ||||
| 
 | ||||
|         binding.retryButton.setOnClickListener(v -> retryUpload()); | ||||
|         binding.cancelButton.setOnClickListener(v -> deleteUpload()); | ||||
|         binding.contributionImage.setOnClickListener(v -> imageClicked()); | ||||
|         binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked()); | ||||
|         binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked()); | ||||
| 
 | ||||
|         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||
|         an upload might take a dozen seconds. */ | ||||
|         AlertDialog.Builder builder = new Builder(parent.getContext()); | ||||
|  | @ -87,14 +71,17 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
| 
 | ||||
|         this.contribution = contribution; | ||||
|         this.position = position; | ||||
|         titleView.setText(contribution.getMedia().getMostRelevantCaption()); | ||||
|         authorView.setText(contribution.getMedia().getAuthor()); | ||||
|         binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption()); | ||||
|         binding.authorView.setText(contribution.getMedia().getAuthor()); | ||||
| 
 | ||||
|         //Removes flicker of loading image. | ||||
|         imageView.getHierarchy().setFadeDuration(0); | ||||
|         binding.contributionImage.getHierarchy().setFadeDuration(0); | ||||
| 
 | ||||
|         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||
|         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||
|          | ||||
|          | ||||
|          | ||||
|         imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||
|         imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||
| 
 | ||||
|         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), | ||||
|             contribution.getLocalUri()); | ||||
|  | @ -103,73 +90,77 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||
|                     .setProgressiveRenderingEnabled(true) | ||||
|                     .build(); | ||||
|             } else if(imageSource != null) { | ||||
|             } | ||||
|             else if (URLUtil.isFileUrl(imageSource)){ | ||||
|                 imageRequest=ImageRequest.fromUri(Uri.parse(imageSource)); | ||||
|             } | ||||
|             else if(imageSource != null) { | ||||
|                 final File file = new File(imageSource); | ||||
|                 imageRequest = ImageRequest.fromFile(file); | ||||
|             } | ||||
| 
 | ||||
|             if(imageRequest != null){ | ||||
|                 imageView.setImageRequest(imageRequest); | ||||
|                 binding.contributionImage.setImageRequest(imageRequest); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         seqNumView.setText(String.valueOf(position + 1)); | ||||
|         seqNumView.setVisibility(View.VISIBLE); | ||||
|         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); | ||||
|         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); | ||||
| 
 | ||||
|         addToWikipediaButton.setVisibility(View.GONE); | ||||
|         binding.wikipediaButton.setVisibility(View.GONE); | ||||
|         switch (contribution.getState()) { | ||||
|             case Contribution.STATE_COMPLETED: | ||||
|                 stateView.setVisibility(View.GONE); | ||||
|                 progressView.setVisibility(View.GONE); | ||||
|                 imageOptions.setVisibility(View.GONE); | ||||
|                 stateView.setText(""); | ||||
|                 binding.contributionState.setVisibility(View.GONE); | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.imageOptions.setVisibility(View.GONE); | ||||
|                 binding.contributionState.setText(""); | ||||
|                 checkIfMediaExistsOnWikipediaPage(contribution); | ||||
|                 break; | ||||
|             case Contribution.STATE_QUEUED: | ||||
|             case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE: | ||||
|                 progressView.setVisibility(View.GONE); | ||||
|                 stateView.setVisibility(View.VISIBLE); | ||||
|                 stateView.setText(R.string.contribution_state_queued); | ||||
|                 imageOptions.setVisibility(View.GONE); | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.contribution_state_queued); | ||||
|                 binding.imageOptions.setVisibility(View.GONE); | ||||
|                 break; | ||||
|             case Contribution.STATE_IN_PROGRESS: | ||||
|                 stateView.setVisibility(View.GONE); | ||||
|                 progressView.setVisibility(View.VISIBLE); | ||||
|                 addToWikipediaButton.setVisibility(View.GONE); | ||||
|                 pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 cancelButton.setVisibility(View.GONE); | ||||
|                 retryButton.setVisibility(View.GONE); | ||||
|                 imageOptions.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setVisibility(View.GONE); | ||||
|                 binding.contributionProgress.setVisibility(View.VISIBLE); | ||||
|                 binding.wikipediaButton.setVisibility(View.GONE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 binding.cancelButton.setVisibility(View.GONE); | ||||
|                 binding.retryButton.setVisibility(View.GONE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 final long total = contribution.getDataLength(); | ||||
|                 final long transferred = contribution.getTransferred(); | ||||
|                 if (transferred == 0 || transferred >= total) { | ||||
|                     progressView.setIndeterminate(true); | ||||
|                     binding.contributionProgress.setIndeterminate(true); | ||||
|                 } else { | ||||
|                     progressView.setIndeterminate(false); | ||||
|                     progressView.setProgress((int) (((double) transferred / (double) total) * 100)); | ||||
|                     binding.contributionProgress.setIndeterminate(false); | ||||
|                     binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100)); | ||||
|                 } | ||||
|                 break; | ||||
|             case Contribution.STATE_PAUSED: | ||||
|                 progressView.setVisibility(View.GONE); | ||||
|                 stateView.setVisibility(View.VISIBLE); | ||||
|                 stateView.setText(R.string.paused); | ||||
|                 cancelButton.setVisibility(View.VISIBLE); | ||||
|                 retryButton.setVisibility(View.GONE); | ||||
|                 pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 imageOptions.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.paused); | ||||
|                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||
|                 binding.retryButton.setVisibility(View.GONE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 setResume(); | ||||
|                 if(pausingPopUp.isShowing()){ | ||||
|                     pausingPopUp.hide(); | ||||
|                 } | ||||
|                 break; | ||||
|             case Contribution.STATE_FAILED: | ||||
|                 stateView.setVisibility(View.VISIBLE); | ||||
|                 stateView.setText(R.string.contribution_state_failed); | ||||
|                 progressView.setVisibility(View.GONE); | ||||
|                 cancelButton.setVisibility(View.VISIBLE); | ||||
|                 retryButton.setVisibility(View.VISIBLE); | ||||
|                 pauseResumeButton.setVisibility(View.GONE); | ||||
|                 imageOptions.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.contribution_state_failed); | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||
|                 binding.retryButton.setVisibility(View.VISIBLE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.GONE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | @ -203,11 +194,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|      */ | ||||
|     private void displayWikipediaButton(Boolean mediaExists) { | ||||
|         if (!mediaExists) { | ||||
|             addToWikipediaButton.setVisibility(View.VISIBLE); | ||||
|             binding.wikipediaButton.setVisibility(View.VISIBLE); | ||||
|             isWikipediaButtonDisplayed = true; | ||||
|             cancelButton.setVisibility(View.GONE); | ||||
|             retryButton.setVisibility(View.GONE); | ||||
|             imageOptions.setVisibility(View.VISIBLE); | ||||
|             binding.cancelButton.setVisibility(View.GONE); | ||||
|             binding.retryButton.setVisibility(View.GONE); | ||||
|             binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -229,7 +220,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      */ | ||||
|     @OnClick(R.id.retryButton) | ||||
|     public void retryUpload() { | ||||
|         callback.retryUpload(contribution); | ||||
|     } | ||||
|  | @ -237,17 +227,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|     /** | ||||
|      * Delete a failed upload attempt | ||||
|      */ | ||||
|     @OnClick(R.id.cancelButton) | ||||
|     public void deleteUpload() { | ||||
|         callback.deleteUpload(contribution); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.contributionImage) | ||||
|     public void imageClicked() { | ||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed); | ||||
|     } | ||||
| 
 | ||||
|     @OnClick(R.id.wikipediaButton) | ||||
|     public void wikipediaButtonClicked() { | ||||
|         callback.addImageToWikipedia(contribution); | ||||
|     } | ||||
|  | @ -255,9 +242,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|     /** | ||||
|      * Triggers a callback for pause/resume | ||||
|      */ | ||||
|     @OnClick(R.id.pauseResumeButton) | ||||
|     public void onPauseResumeButtonClicked() { | ||||
|         if (pauseResumeButton.getTag().toString().equals("pause")) { | ||||
|         if (binding.pauseResumeButton.getTag().toString().equals("pause")) { | ||||
|             pause(); | ||||
|         } else { | ||||
|             resume(); | ||||
|  | @ -279,16 +265,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|      * Update pause/resume button to show pause state | ||||
|      */ | ||||
|     private void setPaused() { | ||||
|         pauseResumeButton.setImageResource(R.drawable.pause_icon); | ||||
|         pauseResumeButton.setTag(parent.getContext().getString(R.string.pause)); | ||||
|         binding.pauseResumeButton.setImageResource(R.drawable.pause_icon); | ||||
|         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update pause/resume button to show resume state | ||||
|      */ | ||||
|     private void setResume() { | ||||
|         pauseResumeButton.setImageResource(R.drawable.play_icon); | ||||
|         pauseResumeButton.setTag(parent.getContext().getString(R.string.resume)); | ||||
|         binding.pauseResumeButton.setImageResource(R.drawable.play_icon); | ||||
|         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume)); | ||||
|     } | ||||
| 
 | ||||
|     public ImageRequest getImageRequest() { | ||||
|  |  | |||
|  | @ -1,14 +1,21 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static android.content.Context.SENSOR_SERVICE; | ||||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; | ||||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; | ||||
| import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; | ||||
| import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.Manifest.permission; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.hardware.Sensor; | ||||
| import android.hardware.SensorEvent; | ||||
| import android.hardware.SensorEventListener; | ||||
| import android.hardware.SensorManager; | ||||
| import android.os.Bundle; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
|  | @ -21,6 +28,9 @@ import android.widget.CheckBox; | |||
| import android.widget.LinearLayout; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
|  | @ -29,17 +39,17 @@ import androidx.fragment.app.FragmentTransaction; | |||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsBinding; | ||||
| import fr.free.nrw.commons.notification.models.Notification; | ||||
| import fr.free.nrw.commons.notification.NotificationController; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import androidx.work.WorkManager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.campaigns.models.Campaign; | ||||
|  | @ -73,18 +83,27 @@ import io.reactivex.schedulers.Schedulers; | |||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class ContributionsFragment | ||||
|         extends CommonsDaggerSupportFragment | ||||
|         implements | ||||
|         OnBackStackChangedListener, | ||||
|         LocationUpdateListener, | ||||
|     extends CommonsDaggerSupportFragment | ||||
|     implements | ||||
|     OnBackStackChangedListener, | ||||
|     LocationUpdateListener, | ||||
|     MediaDetailProvider, | ||||
|     ICampaignsView, ContributionsContract.View, Callback{ | ||||
|     @Inject @Named("default_preferences") JsonKvStore store; | ||||
|     @Inject NearbyController nearbyController; | ||||
|     @Inject OkHttpJsonApiClient okHttpJsonApiClient; | ||||
|     @Inject CampaignsPresenter presenter; | ||||
|     @Inject LocationServiceManager locationManager; | ||||
|     @Inject NotificationController notificationController; | ||||
|     SensorEventListener, | ||||
|     ICampaignsView, ContributionsContract.View, Callback { | ||||
| 
 | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     JsonKvStore store; | ||||
|     @Inject | ||||
|     NearbyController nearbyController; | ||||
|     @Inject | ||||
|     OkHttpJsonApiClient okHttpJsonApiClient; | ||||
|     @Inject | ||||
|     CampaignsPresenter presenter; | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
|     @Inject | ||||
|     NotificationController notificationController; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|  | @ -92,20 +111,18 @@ public class ContributionsFragment | |||
|     private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; | ||||
|     private MediaDetailPagerFragment mediaDetailPagerFragment; | ||||
|     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; | ||||
|     private static final int MAX_RETRIES = 10; | ||||
| 
 | ||||
|     @BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView; | ||||
|     @BindView(R.id.campaigns_view) CampaignView campaignView; | ||||
|     @BindView(R.id.limited_connection_enabled_layout) LinearLayout limitedConnectionEnabledLayout; | ||||
|     @BindView(R.id.limited_connection_description_text_view) TextView limitedConnectionDescriptionTextView; | ||||
| 
 | ||||
|     public FragmentContributionsBinding binding; | ||||
| 
 | ||||
|     @Inject ContributionsPresenter contributionsPresenter; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     private LatLng curLatLng; | ||||
|     private LatLng currentLatLng; | ||||
| 
 | ||||
|     private boolean firstLocationUpdate = true; | ||||
|     private boolean isFragmentAttachedBefore = false; | ||||
|     private View checkBoxView; | ||||
|     private CheckBox checkBox; | ||||
|  | @ -117,6 +134,34 @@ public class ContributionsFragment | |||
|     String userName; | ||||
|     private boolean isUserProfile; | ||||
| 
 | ||||
|     private SensorManager mSensorManager; | ||||
|     private Sensor mLight; | ||||
|     private float direction; | ||||
|     private ActivityResultLauncher<String[]> nearbyLocationPermissionLauncher = registerForActivityResult( | ||||
|         new ActivityResultContracts.RequestMultiplePermissions(), | ||||
|         new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|             @Override | ||||
|             public void onActivityResult(Map<String, Boolean> result) { | ||||
|                 boolean areAllGranted = true; | ||||
|                 for (final boolean b : result.values()) { | ||||
|                     areAllGranted = areAllGranted && b; | ||||
|                 } | ||||
| 
 | ||||
|             if (areAllGranted) { | ||||
|                 onLocationPermissionGranted(); | ||||
|             } else { | ||||
|                 if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                     && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|                     && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|                     && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||
|                     binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|                 } else { | ||||
|                     displayYouWontSeeNearbyMessage(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static ContributionsFragment newInstance() { | ||||
|         ContributionsFragment fragment = new ContributionsFragment(); | ||||
|  | @ -133,17 +178,21 @@ public class ContributionsFragment | |||
|             userName = getArguments().getString(KEY_USERNAME); | ||||
|             isUserProfile = true; | ||||
|         } | ||||
|         mSensorManager = (SensorManager) getActivity().getSystemService(SENSOR_SERVICE); | ||||
|         mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||
|         View view = inflater.inflate(R.layout.fragment_contributions, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
| 
 | ||||
|         binding = FragmentContributionsBinding.inflate(inflater, container, false); | ||||
| 
 | ||||
|         initWLMCampaign(); | ||||
|         presenter.onAttachView(this); | ||||
|         contributionsPresenter.onAttachView(this); | ||||
|         campaignView.setVisibility(View.GONE); | ||||
|         binding.campaignsView.setVisibility(View.GONE); | ||||
|         checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); | ||||
|         checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); | ||||
|         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||
|  | @ -153,6 +202,7 @@ public class ContributionsFragment | |||
|             } | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() | ||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); | ||||
|  | @ -163,13 +213,13 @@ public class ContributionsFragment | |||
| 
 | ||||
|         initFragments(); | ||||
|         if(isUserProfile) { | ||||
|             limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||
|             binding.limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||
|         }else { | ||||
|             upDateUploadCount(); | ||||
|         } | ||||
|         if(shouldShowMediaDetailsFragment){ | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|             showMediaDetailPagerFragment(); | ||||
|         }else{ | ||||
|         } else { | ||||
|             if (mediaDetailPagerFragment != null) { | ||||
|                 removeFragment(mediaDetailPagerFragment); | ||||
|             } | ||||
|  | @ -180,9 +230,9 @@ public class ContributionsFragment | |||
|             && sessionManager.getCurrentAccount() != null && !isUserProfile) { | ||||
|             setUploadCount(); | ||||
|         } | ||||
|         limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); | ||||
|         binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); | ||||
|         setHasOptionsMenu(true); | ||||
|         return view; | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -195,10 +245,13 @@ public class ContributionsFragment | |||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { | ||||
|     public void onCreateOptionsMenu(@NonNull final Menu menu, | ||||
|         @NonNull final MenuInflater inflater) { | ||||
| 
 | ||||
|         // Removing contributions menu items for ProfileActivity | ||||
|         if (getActivity() instanceof ProfileActivity) { return; } | ||||
|         if (getActivity() instanceof ProfileActivity) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         inflater.inflate(R.menu.contribution_activity_notification_menu, menu); | ||||
| 
 | ||||
|  | @ -220,7 +273,7 @@ public class ContributionsFragment | |||
|                 throwable -> Timber.e(throwable, "Error occurred while loading notifications"))); | ||||
|     } | ||||
| 
 | ||||
|     public void scrollToTop( ){ | ||||
|     public void scrollToTop() { | ||||
|         if (contributionsListFragment != null) { | ||||
|             contributionsListFragment.scrollToTop(); | ||||
|         } | ||||
|  | @ -242,22 +295,17 @@ public class ContributionsFragment | |||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||
| 
 | ||||
|         checkable.setChecked(isEnabled); | ||||
|         if (isEnabled) { | ||||
|             limitedConnectionEnabledLayout.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||
|         if (binding!=null) { | ||||
|             binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||
|         } | ||||
| 
 | ||||
|         checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||
|         checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() { | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|                 ((MainActivity) getActivity()).toggleLimitedConnectionMode(); | ||||
|                 boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||
|                 if (isEnabled) { | ||||
|                     limitedConnectionEnabledLayout.setVisibility(View.VISIBLE); | ||||
|                 } else { | ||||
|                     limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||
|                 } | ||||
|                 binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||
|                 checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||
|                 return false; | ||||
|             } | ||||
|  | @ -285,28 +333,31 @@ public class ContributionsFragment | |||
|      */ | ||||
|     private void showContributionsListFragment() { | ||||
|         // show nearby card view on contributions list is visible | ||||
|         if (nearbyNotificationCardView != null && !isUserProfile) { | ||||
|         if (binding.cardViewNearby != null && !isUserProfile) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true)) { | ||||
|                 if (nearbyNotificationCardView.cardViewVisibilityState | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     nearbyNotificationCardView.setVisibility(View.VISIBLE); | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } else { | ||||
|                 nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment); | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment); | ||||
|     } | ||||
| 
 | ||||
|     private void showMediaDetailPagerFragment() { | ||||
|         // hide nearby card view on media detail is visible | ||||
|         setupViewForMediaDetails(); | ||||
|         showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, contributionsListFragment); | ||||
|         showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, | ||||
|             contributionsListFragment); | ||||
|     } | ||||
| 
 | ||||
|     private void setupViewForMediaDetails() { | ||||
|         campaignView.setVisibility(View.GONE); | ||||
|         nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|         if (binding!=null) { | ||||
|             binding.campaignsView.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -328,7 +379,8 @@ public class ContributionsFragment | |||
|             showContributionsListFragment(); | ||||
|         } | ||||
| 
 | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment); | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -351,7 +403,7 @@ public class ContributionsFragment | |||
|             transaction.addToBackStack(tag); | ||||
|             transaction.commit(); | ||||
|             getChildFragmentManager().executePendingTransactions(); | ||||
|         }else if (!fragment.isAdded() && otherFragment != null ) { | ||||
|         } else if (!fragment.isAdded() && otherFragment != null) { | ||||
|             transaction.hide(otherFragment); | ||||
|             transaction.add(R.id.root_frame, fragment, tag); | ||||
|             transaction.addToBackStack(tag); | ||||
|  | @ -376,21 +428,21 @@ public class ContributionsFragment | |||
|     @SuppressWarnings("ConstantConditions") | ||||
|     private void setUploadCount() { | ||||
|         compositeDisposable.add(okHttpJsonApiClient | ||||
|                 .getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(this::displayUploadCount, | ||||
|                         t -> Timber.e(t, "Fetching upload count failed") | ||||
|                 )); | ||||
|             .getUploadCount(((MainActivity) getActivity()).sessionManager.getCurrentAccount().name) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(this::displayUploadCount, | ||||
|                 t -> Timber.e(t, "Fetching upload count failed") | ||||
|             )); | ||||
|     } | ||||
| 
 | ||||
|     private void displayUploadCount(Integer uploadCount) { | ||||
|         if (getActivity().isFinishing() | ||||
|                 || getResources() == null) { | ||||
|             || getResources() == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         ((MainActivity)getActivity()).setNumOfUploads(uploadCount); | ||||
|         ((MainActivity) getActivity()).setNumOfUploads(uploadCount); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -399,6 +451,7 @@ public class ContributionsFragment | |||
|         super.onPause(); | ||||
|         locationManager.removeLocationListener(this); | ||||
|         locationManager.unregisterLocationManager(); | ||||
|         mSensorManager.unregisterListener(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -410,9 +463,13 @@ public class ContributionsFragment | |||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         contributionsPresenter.onAttachView(this); | ||||
|         firstLocationUpdate = true; | ||||
|         locationManager.addLocationListener(this); | ||||
|         nearbyNotificationCardView.permissionRequestButton.setOnClickListener(v -> { | ||||
| 
 | ||||
|         if (binding==null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         binding.cardViewNearby.permissionRequestButton.setOnClickListener(v -> { | ||||
|             showNearbyCardPermissionRationale(); | ||||
|         }); | ||||
| 
 | ||||
|  | @ -420,13 +477,20 @@ public class ContributionsFragment | |||
|         if (mediaDetailPagerFragment == null && !isUserProfile) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true)) { | ||||
|                 checkPermissionsAndShowNearbyCardView(); | ||||
|                 if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     nearbyNotificationCardView.setVisibility(View.VISIBLE); | ||||
| 
 | ||||
|                 // Calling nearby card to keep showing it even when user clicks on it and comes back | ||||
|                 try { | ||||
|                     updateClosestNearbyCardViewInfo(); | ||||
|                 } catch (Exception e) { | ||||
|                     Timber.e(e); | ||||
|                 } | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
| 
 | ||||
|             } else { | ||||
|                 // Hide nearby notification card view if related shared preferences is false | ||||
|                 nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
| 
 | ||||
|             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||
|  | @ -435,83 +499,97 @@ public class ContributionsFragment | |||
|                 fetchCampaigns(); | ||||
|             } | ||||
|         } | ||||
|         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); | ||||
|     } | ||||
| 
 | ||||
|     private void checkPermissionsAndShowNearbyCardView() { | ||||
|         if (PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)) { | ||||
|         if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||
|             onLocationPermissionGranted(); | ||||
|         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                 && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|                 && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|                 && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||
|             nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|             && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|             && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|             && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||
|             binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|             showNearbyCardPermissionRationale(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void requestLocationPermission() { | ||||
|         PermissionUtils.checkPermissionsAndPerformAction(getActivity(), | ||||
|                 Manifest.permission.ACCESS_FINE_LOCATION, | ||||
|                 this::onLocationPermissionGranted, | ||||
|                 this::displayYouWontSeeNearbyMessage, | ||||
|                 -1, | ||||
|                 -1); | ||||
|         nearbyLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION}); | ||||
|     } | ||||
| 
 | ||||
|     private void onLocationPermissionGranted() { | ||||
|         nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; | ||||
|         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; | ||||
|         locationManager.registerLocationManager(); | ||||
|     } | ||||
| 
 | ||||
|     private void showNearbyCardPermissionRationale() { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|                 getString(R.string.nearby_card_permission_title), | ||||
|                 getString(R.string.nearby_card_permission_explanation), | ||||
|                 this::requestLocationPermission, | ||||
|                 this::displayYouWontSeeNearbyMessage, | ||||
|                 checkBoxView, | ||||
|                 false); | ||||
|             getString(R.string.nearby_card_permission_title), | ||||
|             getString(R.string.nearby_card_permission_explanation), | ||||
|             this::requestLocationPermission, | ||||
|             this::displayYouWontSeeNearbyMessage, | ||||
|             checkBoxView, | ||||
|             false); | ||||
|     } | ||||
| 
 | ||||
|     private void displayYouWontSeeNearbyMessage() { | ||||
|         ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.unable_to_display_nearest_place)); | ||||
|         ViewUtil.showLongToast(getActivity(), | ||||
|             getResources().getString(R.string.unable_to_display_nearest_place)); | ||||
|         // Set to true as the user doesn't want the app to ask for location permission anymore | ||||
|         store.putBoolean("doNotAskForLocationPermission", true); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private void updateClosestNearbyCardViewInfo() { | ||||
|         curLatLng = locationManager.getLastLocation(); | ||||
|         currentLatLng = locationManager.getLastLocation(); | ||||
|         compositeDisposable.add(Observable.fromCallable(() -> nearbyController | ||||
|                 .loadAttractionsFromLocation(curLatLng, curLatLng, true, false, false)) // thanks to boolean, it will only return closest result | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(this::updateNearbyNotification, | ||||
|                         throwable -> { | ||||
|                             Timber.d(throwable); | ||||
|                             updateNearbyNotification(null); | ||||
|                         })); | ||||
|                 .loadAttractionsFromLocation(currentLatLng, currentLatLng, true, | ||||
|                     false)) // thanks to boolean, it will only return closest result | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(this::updateNearbyNotification, | ||||
|                 throwable -> { | ||||
|                     Timber.d(throwable); | ||||
|                     updateNearbyNotification(null); | ||||
|                 })); | ||||
|     } | ||||
| 
 | ||||
|     private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { | ||||
|         if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) { | ||||
|             Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0); | ||||
|             String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location); | ||||
|             closestNearbyPlace.setDistance(distance); | ||||
|             nearbyNotificationCardView.updateContent(closestNearbyPlace); | ||||
|     private void updateNearbyNotification( | ||||
|         @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { | ||||
|         if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null | ||||
|             && nearbyPlacesInfo.placeList.size() > 0) { | ||||
|             Place closestNearbyPlace = null; | ||||
|             // Find the first nearby place that has no image and exists | ||||
|             for (Place place : nearbyPlacesInfo.placeList) { | ||||
|                 if (place.pic.equals("") && place.exists) { | ||||
|                     closestNearbyPlace = place; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (closestNearbyPlace == null) { | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } else { | ||||
|                 String distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location); | ||||
|                 closestNearbyPlace.setDistance(distance); | ||||
|                 direction = (float) computeBearing(currentLatLng, closestNearbyPlace.location); | ||||
|                 binding.cardViewNearby.updateContent(closestNearbyPlace); | ||||
|             } | ||||
|         } else { | ||||
|             // Means that no close nearby place is found | ||||
|             nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|             binding.cardViewNearby.setVisibility(View.GONE); | ||||
|         } | ||||
| 
 | ||||
|         // Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731 | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||
|             nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|             binding.cardViewNearby.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         try{ | ||||
|         try { | ||||
|             compositeDisposable.clear(); | ||||
|             getChildFragmentManager().removeOnBackStackChangedListener(this); | ||||
|             locationManager.unregisterLocationManager(); | ||||
|  | @ -525,22 +603,17 @@ public class ContributionsFragment | |||
|     @Override | ||||
|     public void onLocationChangedSignificantly(LatLng latLng) { | ||||
|         // Will be called if location changed more than 1000 meter | ||||
|         // Do nothing on slight changes for using network efficiently | ||||
|         firstLocationUpdate = false; | ||||
|         updateClosestNearbyCardViewInfo(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onLocationChangedSlightly(LatLng latLng) { | ||||
|         /* Update closest nearby notification card onLocationChangedSlightly | ||||
|         If first time to update location after onResume, then no need to wait for significant | ||||
|         location change. Any closest location is better than no location | ||||
|         */ | ||||
|         if (firstLocationUpdate) { | ||||
|          */ | ||||
|         try { | ||||
|             updateClosestNearbyCardViewInfo(); | ||||
|             // Turn it to false, since it is not first location update anymore. To change closest location | ||||
|             // notification, we need to wait for a significant location change. | ||||
|             firstLocationUpdate = false; | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -550,7 +623,8 @@ public class ContributionsFragment | |||
|         updateClosestNearbyCardViewInfo(); | ||||
|     } | ||||
| 
 | ||||
|     @Override public void onViewCreated(@NonNull View view, | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull View view, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|     } | ||||
|  | @ -562,26 +636,35 @@ public class ContributionsFragment | |||
|      */ | ||||
|     private void fetchCampaigns() { | ||||
|         if (Utils.isMonumentsEnabled(new Date())) { | ||||
|             campaignView.setCampaign(wlmCampaign); | ||||
|             campaignView.setVisibility(View.VISIBLE); | ||||
|             if (binding!=null) { | ||||
|                 binding.campaignsView.setCampaign(wlmCampaign); | ||||
|                 binding.campaignsView.setVisibility(View.VISIBLE); | ||||
|             } | ||||
|         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||
|             presenter.getCampaigns(); | ||||
|         } else { | ||||
|             campaignView.setVisibility(View.GONE); | ||||
|             if (binding!=null) { | ||||
|                 binding.campaignsView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override public void showMessage(String message) { | ||||
|     @Override | ||||
|     public void showMessage(String message) { | ||||
|         Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); | ||||
|     } | ||||
| 
 | ||||
|     @Override public void showCampaigns(Campaign campaign) { | ||||
|     @Override | ||||
|     public void showCampaigns(Campaign campaign) { | ||||
|         if (campaign != null && !isUserProfile) { | ||||
|             campaignView.setCampaign(campaign); | ||||
|             if (binding!=null) { | ||||
|                 binding.campaignsView.setCampaign(campaign); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override public void onDestroyView() { | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         presenter.onDetachView(); | ||||
|     } | ||||
|  | @ -593,6 +676,17 @@ public class ContributionsFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload process for a contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     public void restartUpload(Contribution contribution) { | ||||
|         contribution.setState(Contribution.STATE_QUEUED); | ||||
|         contributionsPresenter.saveContribution(contribution); | ||||
|         Timber.d("Restarting for %s", contribution.toString()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      * | ||||
|  | @ -601,10 +695,25 @@ public class ContributionsFragment | |||
|     @Override | ||||
|     public void retryUpload(Contribution contribution) { | ||||
|         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { | ||||
|             if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { | ||||
|                 contribution.setState(Contribution.STATE_QUEUED); | ||||
|                 contributionsPresenter.saveContribution(contribution); | ||||
|                 Timber.d("Restarting for %s", contribution.toString()); | ||||
|             if (contribution.getState() == STATE_PAUSED | ||||
|                 || contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { | ||||
|                 restartUpload(contribution); | ||||
|             } else if (contribution.getState() == STATE_FAILED) { | ||||
|                 int retries = contribution.getRetries(); | ||||
|                 // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 | ||||
|                 /* Limit the number of retries for a failed upload | ||||
|                    to handle cases like invalid filename as such uploads | ||||
|                    will never be successful */ | ||||
|                 if (retries < MAX_RETRIES) { | ||||
|                     contribution.setRetries(retries + 1); | ||||
|                     Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), | ||||
|                         retries + 1); | ||||
|                     restartUpload(contribution); | ||||
|                 } else { | ||||
|                     // TODO: Show the exact reason for failure | ||||
|                     Toast.makeText(getContext(), | ||||
|                         R.string.retry_limit_reached, Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|             } else { | ||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); | ||||
|             } | ||||
|  | @ -616,6 +725,7 @@ public class ContributionsFragment | |||
| 
 | ||||
|     /** | ||||
|      * Pauses the upload | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|  | @ -639,15 +749,15 @@ public class ContributionsFragment | |||
| 
 | ||||
|     /** | ||||
|      * Replace whatever is in the current contributionsFragmentContainer view with | ||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a | ||||
|      * contribution. | ||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||
|      * a contribution. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showDetail(int position, boolean isWikipediaButtonDisplayed) { | ||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { | ||||
|             mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); | ||||
|             if(isUserProfile) { | ||||
|                 ((ProfileActivity)getActivity()).setScroll(false); | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             if (isUserProfile) { | ||||
|                 ((ProfileActivity) getActivity()).setScroll(false); | ||||
|             } | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } | ||||
|  | @ -672,24 +782,26 @@ public class ContributionsFragment | |||
|     public boolean backButtonClicked() { | ||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { | ||||
|                 if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     nearbyNotificationCardView.setVisibility(View.VISIBLE); | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } else { | ||||
|                 nearbyNotificationCardView.setVisibility(View.GONE); | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
|             removeFragment(mediaDetailPagerFragment); | ||||
|             showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment); | ||||
|             if(isUserProfile) { | ||||
|             showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|                 mediaDetailPagerFragment); | ||||
|             if (isUserProfile) { | ||||
|                 // Fragment is associated with ProfileActivity | ||||
|                 // Enable ParentViewPager Scroll | ||||
|                 ((ProfileActivity)getActivity()).setScroll(true); | ||||
|             }else { | ||||
|                 ((ProfileActivity) getActivity()).setScroll(true); | ||||
|             } else { | ||||
|                 fetchCampaigns(); | ||||
|             } | ||||
|             if (getActivity() instanceof MainActivity) { | ||||
|                 // Fragment is associated with MainActivity | ||||
|                 ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||
|                     .setDisplayHomeAsUpEnabled(false); | ||||
|                 ((MainActivity) getActivity()).showTabs(); | ||||
|             } | ||||
|             return true; | ||||
|  | @ -709,11 +821,11 @@ public class ContributionsFragment | |||
|     void upDateUploadCount() { | ||||
|         WorkManager.getInstance(getContext()) | ||||
|             .getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe( | ||||
|             getViewLifecycleOwner(), workInfos -> { | ||||
|                 if (workInfos.size() > 0) { | ||||
|                     setUploadCount(); | ||||
|                 } | ||||
|             }); | ||||
|                 getViewLifecycleOwner(), workInfos -> { | ||||
|                     if (workInfos.size() > 0) { | ||||
|                         setUploadCount(); | ||||
|                     } | ||||
|                 }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -724,29 +836,40 @@ public class ContributionsFragment | |||
|      */ | ||||
|     @Override | ||||
|     public void refreshNominatedMedia(int index) { | ||||
|         if(mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||
|             removeFragment(mediaDetailPagerFragment); | ||||
|             mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             mediaDetailPagerFragment.showImage(index); | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   // click listener to toggle description that means uses can press the limited connection | ||||
|   // banner and description will hide. Tap again to show description. | ||||
|   private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { | ||||
|     // click listener to toggle description that means uses can press the limited connection | ||||
|     // banner and description will hide. Tap again to show description. | ||||
|     private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { | ||||
| 
 | ||||
|       @Override | ||||
|       public void onClick(View view) { | ||||
|           View view2 = limitedConnectionDescriptionTextView; | ||||
|           if (view2.getVisibility() == View.GONE) { | ||||
|               view2.setVisibility(View.VISIBLE); | ||||
|           } else { | ||||
|               view2.setVisibility(View.GONE); | ||||
|           } | ||||
|       } | ||||
|   }; | ||||
|         @Override | ||||
|         public void onClick(View view) { | ||||
|             View view2 = binding.limitedConnectionDescriptionTextView; | ||||
|             if (view2.getVisibility() == View.GONE) { | ||||
|                 view2.setVisibility(View.VISIBLE); | ||||
|             } else { | ||||
|                 view2.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * When the device rotates, rotate the Nearby banner's compass arrow in tandem. | ||||
|      */ | ||||
|     @Override | ||||
|     public void onSensorChanged(SensorEvent event) { | ||||
|         float rotateDegree = Math.round(event.values[0]); | ||||
|         binding.cardViewNearby.rotateCompass(rotateDegree, direction); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAccuracyChanged(Sensor sensor, int accuracy) { | ||||
|         // Nothing to do. | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import fr.free.nrw.commons.BasePresenter; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * The contract for Contributions list View & Presenter | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import static android.view.View.GONE; | |||
| import static android.view.View.VISIBLE; | ||||
| import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.content.Context; | ||||
| import android.content.res.Configuration; | ||||
| import android.net.Uri; | ||||
|  | @ -16,11 +17,12 @@ import android.view.ViewGroup; | |||
| import android.view.animation.Animation; | ||||
| import android.view.animation.AnimationUtils; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.TextView; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.widget.AppCompatTextView; | ||||
| import androidx.annotation.VisibleForTesting; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.recyclerview.widget.GridLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
|  | @ -28,26 +30,25 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; | |||
| import androidx.recyclerview.widget.RecyclerView.ItemAnimator; | ||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | ||||
| import androidx.recyclerview.widget.SimpleItemAnimator; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import butterknife.OnClick; | ||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsListBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.wikipedia.dataclient.WikiSite; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  | @ -60,63 +61,72 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
| 
 | ||||
|     private static final String RV_STATE = "rv_scroll_state"; | ||||
| 
 | ||||
|     @BindView(R.id.contributionsList) | ||||
|     RecyclerView rvContributionsList; | ||||
|     @BindView(R.id.loadingContributionsProgressBar) | ||||
|     ProgressBar progressBar; | ||||
|     @BindView(R.id.fab_plus) | ||||
|     FloatingActionButton fabPlus; | ||||
|     @BindView(R.id.fab_camera) | ||||
|     FloatingActionButton fabCamera; | ||||
|     @BindView(R.id.fab_gallery) | ||||
|     FloatingActionButton fabGallery; | ||||
|     @BindView(R.id.noContributionsYet) | ||||
|     TextView noContributionsYet; | ||||
|     @BindView(R.id.fab_layout) | ||||
|     LinearLayout fab_layout; | ||||
|     @BindView(R.id.fab_custom_gallery) | ||||
|     FloatingActionButton fabCustomGallery; | ||||
| 
 | ||||
|     @Inject | ||||
|     SystemThemeUtils systemThemeUtils; | ||||
|     @BindView(R.id.tv_contributions_of_user) | ||||
|     AppCompatTextView tvContributionsOfUser; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionController controller; | ||||
|     @Inject | ||||
|     MediaClient mediaClient; | ||||
| 
 | ||||
|     @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) | ||||
|     @Inject | ||||
|     WikiSite languageWikipediaSite; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsListPresenter contributionsListPresenter; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     private FragmentContributionsListBinding binding; | ||||
|     private Animation fab_close; | ||||
|     private Animation fab_open; | ||||
|     private Animation rotate_forward; | ||||
|     private Animation rotate_backward; | ||||
| 
 | ||||
| 
 | ||||
|     private boolean isFabOpen; | ||||
| 
 | ||||
|     private ContributionsListAdapter adapter; | ||||
|     @VisibleForTesting | ||||
|     protected RecyclerView rvContributionsList; | ||||
| 
 | ||||
|     @Nullable private Callback callback; | ||||
|     @VisibleForTesting | ||||
|     protected ContributionsListAdapter adapter; | ||||
| 
 | ||||
|     @Nullable | ||||
|     @VisibleForTesting | ||||
|     protected Callback callback; | ||||
| 
 | ||||
|     private final int SPAN_COUNT_LANDSCAPE = 3; | ||||
|     private final int SPAN_COUNT_PORTRAIT = 1; | ||||
| 
 | ||||
|     private int contributionsSize; | ||||
|     String userName; | ||||
|     private String userName; | ||||
| 
 | ||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( | ||||
|         new ActivityResultContracts.RequestMultiplePermissions(), | ||||
|         new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|             @Override | ||||
|             public void onActivityResult(Map<String, Boolean> result) { | ||||
|                 boolean areAllGranted = true; | ||||
|                 for (final boolean b : result.values()) { | ||||
|                     areAllGranted = areAllGranted && b; | ||||
|                 } | ||||
| 
 | ||||
|                 if (areAllGranted) { | ||||
|                     controller.locationPermissionCallback.onLocationPermissionGranted(); | ||||
|                 } else { | ||||
|                     if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                         controller.handleShowRationaleFlowCameraLocation(getActivity(), | ||||
|                             inAppCameraLocationPermissionLauncher); | ||||
|                     } else { | ||||
|                         controller.locationPermissionCallback.onLocationPermissionDenied( | ||||
|                             getActivity().getString( | ||||
|                                 R.string.in_app_camera_location_permission_denied)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) { | ||||
|     public void onCreate( | ||||
|         @Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         //Now that we are allowing this fragment to be started for | ||||
|         // any userName- we expect it to be passed as an argument | ||||
|  | @ -133,21 +143,36 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     public View onCreateView( | ||||
|         final LayoutInflater inflater, @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         binding = FragmentContributionsListBinding.inflate( | ||||
|             inflater, container, false | ||||
|         ); | ||||
|         rvContributionsList = binding.contributionsList; | ||||
| 
 | ||||
|         contributionsListPresenter.onAttachView(this); | ||||
|         binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector()); | ||||
|         binding.fabCustomGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.custom_selector_title); | ||||
|             return true; | ||||
|         }); | ||||
| 
 | ||||
|         if (Objects.equals(sessionManager.getUserName(), userName)) { | ||||
|             tvContributionsOfUser.setVisibility(GONE); | ||||
|             fab_layout.setVisibility(VISIBLE); | ||||
|             binding.tvContributionsOfUser.setVisibility(GONE); | ||||
|             binding.fabLayout.setVisibility(VISIBLE); | ||||
|         } else { | ||||
|             tvContributionsOfUser.setVisibility(VISIBLE); | ||||
|             tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName)); | ||||
|             fab_layout.setVisibility(GONE); | ||||
|             binding.tvContributionsOfUser.setVisibility(VISIBLE); | ||||
|             binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName)); | ||||
|             binding.fabLayout.setVisibility(GONE); | ||||
|         } | ||||
| 
 | ||||
|         initAdapter(); | ||||
|         return view; | ||||
| 
 | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         binding = null; | ||||
|         super.onDestroyView(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -280,7 +305,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     public void onConfigurationChanged(final Configuration newConfig) { | ||||
|         super.onConfigurationChanged(newConfig); | ||||
|         // check orientation | ||||
|         fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|         binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|             LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); | ||||
|         rvContributionsList | ||||
|             .setLayoutManager( | ||||
|  | @ -295,22 +320,29 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     } | ||||
| 
 | ||||
|     private void setListeners() { | ||||
|         fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); | ||||
|         fabCamera.setOnClickListener(view -> { | ||||
|             controller.initiateCameraPick(getActivity()); | ||||
|         binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); | ||||
|         binding.fabCamera.setOnClickListener(view -> { | ||||
|             controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher); | ||||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         fabGallery.setOnClickListener(view -> { | ||||
|         binding.fabCamera.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera); | ||||
|             return true; | ||||
|         }); | ||||
|         binding.fabGallery.setOnClickListener(view -> { | ||||
|             controller.initiateGalleryPick(getActivity(), true); | ||||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         binding.fabGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery); | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch Custom Selector. | ||||
|      */ | ||||
|     @OnClick(R.id.fab_custom_gallery) | ||||
|     void launchCustomSelector(){ | ||||
|     protected void launchCustomSelector() { | ||||
|         controller.initiateCustomGalleryPickWithPermission(getActivity()); | ||||
|         animateFAB(isFabOpen); | ||||
|     } | ||||
|  | @ -321,25 +353,25 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
| 
 | ||||
|     private void animateFAB(final boolean isFabOpen) { | ||||
|         this.isFabOpen = !isFabOpen; | ||||
|         if (fabPlus.isShown()) { | ||||
|         if (isFabOpen) { | ||||
|             fabPlus.startAnimation(rotate_backward); | ||||
|             fabCamera.startAnimation(fab_close); | ||||
|             fabGallery.startAnimation(fab_close); | ||||
|             fabCustomGallery.startAnimation(fab_close); | ||||
|             fabCamera.hide(); | ||||
|             fabGallery.hide(); | ||||
|             fabCustomGallery.hide(); | ||||
|         } else { | ||||
|             fabPlus.startAnimation(rotate_forward); | ||||
|             fabCamera.startAnimation(fab_open); | ||||
|             fabGallery.startAnimation(fab_open); | ||||
|             fabCustomGallery.startAnimation(fab_open); | ||||
|             fabCamera.show(); | ||||
|             fabGallery.show(); | ||||
|             fabCustomGallery.show(); | ||||
|         } | ||||
|         this.isFabOpen = !isFabOpen; | ||||
|         if (binding.fabPlus.isShown()) { | ||||
|             if (isFabOpen) { | ||||
|                 binding.fabPlus.startAnimation(rotate_backward); | ||||
|                 binding.fabCamera.startAnimation(fab_close); | ||||
|                 binding.fabGallery.startAnimation(fab_close); | ||||
|                 binding.fabCustomGallery.startAnimation(fab_close); | ||||
|                 binding.fabCamera.hide(); | ||||
|                 binding.fabGallery.hide(); | ||||
|                 binding.fabCustomGallery.hide(); | ||||
|             } else { | ||||
|                 binding.fabPlus.startAnimation(rotate_forward); | ||||
|                 binding.fabCamera.startAnimation(fab_open); | ||||
|                 binding.fabGallery.startAnimation(fab_open); | ||||
|                 binding.fabCustomGallery.startAnimation(fab_open); | ||||
|                 binding.fabCamera.show(); | ||||
|                 binding.fabGallery.show(); | ||||
|                 binding.fabCustomGallery.show(); | ||||
|             } | ||||
|             this.isFabOpen = !isFabOpen; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -348,7 +380,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|      */ | ||||
|     @Override | ||||
|     public void showWelcomeTip(final boolean shouldShow) { | ||||
|         noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|         binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -358,12 +390,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|      */ | ||||
|     @Override | ||||
|     public void showProgress(final boolean shouldShow) { | ||||
|         progressBar.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|         binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showNoContributionsUI(final boolean shouldShow) { | ||||
|         noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|         binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -393,14 +425,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     @Override | ||||
|     public void deleteUpload(final Contribution contribution) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             String.format(getString(R.string.cancelling_upload), | ||||
|                 Locale.getDefault().getDisplayLanguage()), | ||||
|             String.format(getString(R.string.cancel_upload_dialog), | ||||
|                 Locale.getDefault().getDisplayLanguage()), | ||||
|             "YES", "NO", | ||||
|             String.format(Locale.getDefault(), | ||||
|                 getString(R.string.cancelling_upload)), | ||||
|             String.format(Locale.getDefault(), | ||||
|                 getString(R.string.cancel_upload_dialog)), | ||||
|             String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)), | ||||
|             () -> { | ||||
|                 ViewUtil.showShortToast(getContext(), R.string.cancelling_upload); | ||||
|                 contributionsListPresenter.deleteUpload(contribution); | ||||
|                 CommonsApplication.cancelledUploads.add(contribution.getPageId()); | ||||
|             }, () -> { | ||||
|                 // Do nothing | ||||
|             }); | ||||
|  | @ -422,8 +455,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     public void addImageToWikipedia(Contribution contribution) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             getString(R.string.add_picture_to_wikipedia_article_title), | ||||
|             String.format(getString(R.string.add_picture_to_wikipedia_article_desc), | ||||
|                 Locale.getDefault().getDisplayLanguage()), | ||||
|             getString(R.string.add_picture_to_wikipedia_article_desc), | ||||
|             () -> { | ||||
|                 showAddImageToWikipediaInstructions(contribution); | ||||
|             }, () -> { | ||||
|  |  | |||
|  | @ -1,18 +1,12 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import androidx.work.OneTimeWorkRequest; | ||||
| import androidx.work.WorkManager; | ||||
| import fr.free.nrw.commons.MediaDataExtractor; | ||||
| import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.functions.Action; | ||||
| import io.reactivex.functions.Consumer; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
|  | @ -76,11 +70,7 @@ public class ContributionsPresenter implements UserActionListener { | |||
|         compositeDisposable.add(repository | ||||
|             .save(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe(() -> { | ||||
|                 WorkManager.getInstance(view.getContext().getApplicationContext()) | ||||
|                     .enqueueUniqueWork( | ||||
|                         UploadWorker.class.getSimpleName(), | ||||
|                         ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); | ||||
|             })); | ||||
|             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                 view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP))); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,28 +1,24 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Build.VERSION; | ||||
| import android.os.Build.VERSION_CODES; | ||||
| import android.os.Bundle; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.widget.FrameLayout; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.widget.Toolbar; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import androidx.work.OneTimeWorkRequest; | ||||
| import androidx.work.WorkManager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.databinding.MainBinding; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.WelcomeActivity; | ||||
|  | @ -45,9 +41,13 @@ import fr.free.nrw.commons.notification.NotificationController; | |||
| import fr.free.nrw.commons.quiz.QuizChecker; | ||||
| import fr.free.nrw.commons.settings.SettingsFragment; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
|  | @ -59,14 +59,8 @@ public class MainActivity  extends BaseActivity | |||
|     SessionManager sessionManager; | ||||
|     @Inject | ||||
|     ContributionController controller; | ||||
|     @BindView(R.id.toolbar) | ||||
|     Toolbar toolbar; | ||||
|     @BindView(R.id.pager) | ||||
|     public UnswipableViewPager viewPager; | ||||
|     @BindView(R.id.fragmentContainer) | ||||
|     public FrameLayout fragmentContainer; | ||||
|     @BindView(R.id.fragment_main_nav_tab_layout) | ||||
|     NavTabLayout tabLayout; | ||||
|     @Inject | ||||
|     ContributionDao contributionDao; | ||||
| 
 | ||||
|     private ContributionsFragment contributionsFragment; | ||||
|     private NearbyParentFragment nearbyParentFragment; | ||||
|  | @ -91,6 +85,11 @@ public class MainActivity  extends BaseActivity | |||
| 
 | ||||
|     public Menu menu; | ||||
| 
 | ||||
|     public MainBinding binding; | ||||
| 
 | ||||
|     NavTabLayout tabLayout; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Consumers should be simply using this method to use this activity. | ||||
|      * | ||||
|  | @ -118,11 +117,13 @@ public class MainActivity  extends BaseActivity | |||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         binding = MainBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
|         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||
|         tabLayout = binding.fragmentMainNavTabLayout; | ||||
|         loadLocale(); | ||||
|         setContentView(R.layout.main); | ||||
|         ButterKnife.bind(this); | ||||
|         setSupportActionBar(toolbar); | ||||
|         toolbar.setNavigationOnClickListener(view -> { | ||||
| 
 | ||||
|         binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { | ||||
|             onSupportNavigateUp(); | ||||
|         }); | ||||
|         /* | ||||
|  | @ -139,6 +140,10 @@ public class MainActivity  extends BaseActivity | |||
|             setTitle(getString(R.string.navigation_item_explore)); | ||||
|             setUpLoggedOutPager(); | ||||
|         } else { | ||||
|             if (applicationKvStore.getBoolean("firstrun", true)) { | ||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); | ||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); | ||||
|             } | ||||
|             if(savedInstanceState == null){ | ||||
|                 //starting a fresh fragment. | ||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions | ||||
|  | @ -152,15 +157,29 @@ public class MainActivity  extends BaseActivity | |||
|                 } | ||||
|             } | ||||
|             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) { | ||||
|                 PermissionUtils.checkPermissionsAndPerformAction( | ||||
|                     this, | ||||
|                     () -> {}, | ||||
|                     R.string.media_location_permission_denied, | ||||
|                     R.string.add_location_manually, | ||||
|                     permission.ACCESS_MEDIA_LOCATION); | ||||
|             } | ||||
|             checkAndResumeStuckUploads(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void setSelectedItemId(int id) { | ||||
|         tabLayout.setSelectedItemId(id); | ||||
|         binding.fragmentMainNavTabLayout.setSelectedItemId(id); | ||||
|     } | ||||
| 
 | ||||
|     private void setUpPager() { | ||||
|         tabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { | ||||
|             if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                 // do not change title for more fragment | ||||
|                 setTitle(item.getTitle()); | ||||
|  | @ -175,7 +194,7 @@ public class MainActivity  extends BaseActivity | |||
| 
 | ||||
|     private void setUpLoggedOutPager() { | ||||
|         loadFragment(ExploreFragment.newInstance(),false); | ||||
|         tabLayout.setOnNavigationItemSelectedListener(item -> { | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { | ||||
|             if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                 // do not change title for more fragment | ||||
|                 setTitle(item.getTitle()); | ||||
|  | @ -237,11 +256,11 @@ public class MainActivity  extends BaseActivity | |||
|     } | ||||
| 
 | ||||
|     public void hideTabs() { | ||||
|         tabLayout.setVisibility(View.GONE); | ||||
|         binding.fragmentMainNavTabLayout.setVisibility(View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     public void showTabs() { | ||||
|         tabLayout.setVisibility(View.VISIBLE); | ||||
|         binding.fragmentMainNavTabLayout.setVisibility(View.VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -259,6 +278,34 @@ public class MainActivity  extends BaseActivity | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resume the uploads that got stuck because of the app being killed | ||||
|      * or the device being rebooted. | ||||
|      * | ||||
|      * When the app is terminated or the device is restarted, contributions remain in the | ||||
|      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. | ||||
|      * So, retrieving contributions labeled as 'STATE_IN_PROGRESS' | ||||
|      * from the database will provide the list of uploads that appear as stuck on opening the app again | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void checkAndResumeStuckUploads() { | ||||
|         List<Contribution> stuckUploads = contributionDao.getContribution( | ||||
|                 Collections.singletonList(Contribution.STATE_IN_PROGRESS)) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .blockingGet(); | ||||
|         Timber.d("Resuming " + stuckUploads.size() + " uploads..."); | ||||
|         if(!stuckUploads.isEmpty()) { | ||||
|             for(Contribution contribution: stuckUploads) { | ||||
|                 contribution.setState(Contribution.STATE_QUEUED); | ||||
|                 Completable.fromAction(() -> contributionDao.saveSynchronous(contribution)) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .subscribe(); | ||||
|             } | ||||
|             WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                 this, ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onPostCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onPostCreate(savedInstanceState); | ||||
|  | @ -268,7 +315,7 @@ public class MainActivity  extends BaseActivity | |||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem()); | ||||
|         outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem()); | ||||
|         outState.putString("activeFragment", activeFragment.name()); | ||||
|     } | ||||
| 
 | ||||
|  | @ -347,6 +394,21 @@ public class MainActivity  extends BaseActivity | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry all failed uploads as soon as the user returns to the app | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void retryAllFailedUploads() { | ||||
|         contributionDao. | ||||
|             getContribution(Collections.singletonList(Contribution.STATE_FAILED)) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe(failedUploads -> { | ||||
|                 for (Contribution contribution: failedUploads) { | ||||
|                     contributionsFragment.retryUpload(contribution); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     public void toggleLimitedConnectionMode() { | ||||
|         defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, | ||||
|             !defaultKvStore | ||||
|  | @ -356,10 +418,8 @@ public class MainActivity  extends BaseActivity | |||
|             viewUtilWrapper | ||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); | ||||
|         } else { | ||||
|             WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork( | ||||
|                 UploadWorker.class.getSimpleName(), | ||||
|                 ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class)); | ||||
| 
 | ||||
|             WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), | ||||
|                 ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||
|             viewUtilWrapper | ||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); | ||||
|         } | ||||
|  | @ -368,8 +428,6 @@ public class MainActivity  extends BaseActivity | |||
|     public void centerMapToPlace(Place place) { | ||||
|         setSelectedItemId(NavTab.NEARBY.code()); | ||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() { | ||||
|             // if mapBox initialize in nearbyParentFragment then MapReady() function called | ||||
|             // so that nearbyParentFragemt.centerMaptoPlace(place) not throw any null pointer exception | ||||
|             @Override | ||||
|             public void onReady() { | ||||
|                 nearbyParentFragment.centerMapToPlace(place); | ||||
|  | @ -390,8 +448,11 @@ public class MainActivity  extends BaseActivity | |||
| 
 | ||||
|         if ((applicationKvStore.getBoolean("firstrun", true)) && | ||||
|             (!applicationKvStore.getBoolean("login_skipped"))) { | ||||
|             defaultKvStore.putBoolean("inAppCameraFirstRun", true); | ||||
|             WelcomeActivity.startYourself(this); | ||||
|         } | ||||
| 
 | ||||
|         retryAllFailedUploads(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -407,7 +468,7 @@ public class MainActivity  extends BaseActivity | |||
|      * Public method to show nearby from the reference of this. | ||||
|      */ | ||||
|     public void showNearby() { | ||||
|         tabLayout.setSelectedItemId(NavTab.NEARBY.code()); | ||||
|         binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code()); | ||||
|     } | ||||
| 
 | ||||
|     public enum ActiveFragment { | ||||
|  |  | |||
|  | @ -0,0 +1,130 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.app.NotificationChannel; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.WallpaperManager; | ||||
| import android.content.Context; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.util.Log; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import androidx.work.Data; | ||||
| import androidx.work.Worker; | ||||
| import androidx.work.WorkerParameters; | ||||
| import com.facebook.common.executors.CallerThreadExecutor; | ||||
| import com.facebook.common.references.CloseableReference; | ||||
| import com.facebook.datasource.DataSource; | ||||
| import com.facebook.drawee.backends.pipeline.Fresco; | ||||
| import com.facebook.imagepipeline.core.ImagePipeline; | ||||
| import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; | ||||
| import com.facebook.imagepipeline.image.CloseableImage; | ||||
| import com.facebook.imagepipeline.request.ImageRequest; | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder; | ||||
| import fr.free.nrw.commons.R; | ||||
| import java.io.IOException; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class SetWallpaperWorker extends Worker { | ||||
| 
 | ||||
|     private static final String NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel"; | ||||
|     private static final int NOTIFICATION_ID = 1; | ||||
| 
 | ||||
|     public SetWallpaperWorker(@NonNull Context context, @NonNull WorkerParameters params) { | ||||
|         super(context, params); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Result doWork() { | ||||
|         Context context = getApplicationContext(); | ||||
|         createNotificationChannel(context); | ||||
|         showProgressNotification(context); | ||||
| 
 | ||||
|         String imageUrl = getInputData().getString("imageUrl"); | ||||
|         if (imageUrl == null) { | ||||
|             return Result.failure(); | ||||
|         } | ||||
| 
 | ||||
|         ImageRequest imageRequest = ImageRequestBuilder | ||||
|             .newBuilderWithSource(Uri.parse(imageUrl)) | ||||
|             .build(); | ||||
| 
 | ||||
|         ImagePipeline imagePipeline = Fresco.getImagePipeline(); | ||||
|         final DataSource<CloseableReference<CloseableImage>> | ||||
|             dataSource = imagePipeline.fetchDecodedImage(imageRequest, context); | ||||
| 
 | ||||
|         dataSource.subscribe(new BaseBitmapDataSubscriber() { | ||||
|             @Override | ||||
|             public void onNewResultImpl(@Nullable Bitmap bitmap) { | ||||
|                 if (dataSource.isFinished() && bitmap != null) { | ||||
|                     Timber.d("Bitmap loaded from url %s", imageUrl.toString()); | ||||
|                     setWallpaper(context, Bitmap.createBitmap(bitmap)); | ||||
|                     dataSource.close(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailureImpl(DataSource dataSource) { | ||||
|                 Timber.d("Error getting bitmap from image url %s", imageUrl.toString()); | ||||
|                 showNotification(context, "Setting Wallpaper Failed", "Failed to download image."); | ||||
|                 if (dataSource != null) { | ||||
|                     dataSource.close(); | ||||
|                 } | ||||
|             } | ||||
|         }, CallerThreadExecutor.getInstance()); | ||||
| 
 | ||||
|         return Result.success(); | ||||
|     } | ||||
| 
 | ||||
|     private  void setWallpaper(Context context, Bitmap bitmap) { | ||||
|         WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); | ||||
| 
 | ||||
|         try { | ||||
|             wallpaperManager.setBitmap(bitmap); | ||||
|             showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully."); | ||||
| 
 | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e, "Error setting wallpaper"); | ||||
|             showNotification(context, "Setting Wallpaper Failed", " "+e.getLocalizedMessage()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void showProgressNotification(Context context) { | ||||
|         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle("Setting Wallpaper") | ||||
|             .setContentText("Please wait...") | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(true) | ||||
|             .setProgress(0, 0, true); | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private void showNotification(Context context, String title, String content) { | ||||
|         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle(title) | ||||
|             .setContentText(content) | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(false); | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private void createNotificationChannel(Context context) { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             CharSequence name = "Wallpaper Setting"; | ||||
|             String description = "Notifications for wallpaper setting progress"; | ||||
|             int importance = NotificationManager.IMPORTANCE_HIGH; | ||||
|             NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance); | ||||
|             channel.setDescription(description); | ||||
|             NotificationManager notificationManager = context.getSystemService(NotificationManager.class); | ||||
|             notificationManager.createNotificationChannel(channel); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -16,7 +16,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { | |||
|     private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 | ||||
|     private val SWIPE_VELOCITY_THRESHOLD = 1000 | ||||
| 
 | ||||
|     override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean { | ||||
|     override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean { | ||||
|         return gestureDetector.onTouchEvent(motionEvent) | ||||
|     } | ||||
| 
 | ||||
|  | @ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { | |||
| 
 | ||||
|     inner class GestureListener : GestureDetector.SimpleOnGestureListener() { | ||||
| 
 | ||||
|         override fun onDown(e: MotionEvent?): Boolean { | ||||
|         override fun onDown(e: MotionEvent): Boolean { | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView | |||
| import com.bumptech.glide.Glide | ||||
| import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY | ||||
|  | @ -85,6 +86,8 @@ class ImageAdapter( | |||
|      */ | ||||
|     private var actionableImagesMap: TreeMap<Int, Image> = TreeMap() | ||||
| 
 | ||||
|     private var uploadingContributionList: List<Contribution> = ArrayList() | ||||
| 
 | ||||
|     /** | ||||
|      * Stores already added positions of actionable images | ||||
|      */ | ||||
|  | @ -119,6 +122,7 @@ class ImageAdapter( | |||
|      * Bind View holder, load image, selected view, click listeners. | ||||
|      */ | ||||
|     override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { | ||||
| 
 | ||||
|         var image=images[position] | ||||
|         holder.image.setImageDrawable (null) | ||||
|         if (context.contentResolver.getType(image.uri) == null) { | ||||
|  | @ -151,13 +155,12 @@ class ImageAdapter( | |||
| 
 | ||||
|             val isSelected = selectedIndex != -1 | ||||
|             if (isSelected) { | ||||
|                 holder.itemSelected(selectedImages.size) | ||||
|                 holder.itemSelected() | ||||
|             } else { | ||||
|                 holder.itemUnselected() | ||||
|             } | ||||
| 
 | ||||
|             imageLoader.queryAndSetView( | ||||
|                 holder, image, ioDispatcher, defaultDispatcher | ||||
|                 holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList | ||||
|             ) | ||||
|             scope.launch { | ||||
|                 val sharedPreferences: SharedPreferences = | ||||
|  | @ -168,15 +171,17 @@ class ImageAdapter( | |||
|                     // If the position is not already visited, that means the position is new then | ||||
|                     // finds the next actionable image position from all images | ||||
|                     if (!alreadyAddedPositions.contains(position)) { | ||||
|                         processThumbnailForActionedImage(holder, position) | ||||
|                         processThumbnailForActionedImage(holder, position, uploadingContributionList) | ||||
| 
 | ||||
|                     // If the position is already visited, that means the image is already present | ||||
|                     // inside map, so it will fetch the image from the map and load in the holder | ||||
|                     } else { | ||||
|                         val actionableImages: List<Image> = ArrayList(actionableImagesMap.values) | ||||
|                         image = actionableImages[position] | ||||
|                         Glide.with(holder.image).load(image.uri) | ||||
|                             .thumbnail(0.3f).into(holder.image) | ||||
|                         if(actionableImages.size > position) { | ||||
|                             image = actionableImages[position] | ||||
|                             Glide.with(holder.image).load(image.uri) | ||||
|                                 .thumbnail(0.3f).into(holder.image) | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                 // If switch is turned off, it just fetches the image from all images without any | ||||
|  | @ -204,11 +209,12 @@ class ImageAdapter( | |||
|      */ | ||||
|     suspend fun processThumbnailForActionedImage( | ||||
|         holder: ImageViewHolder, | ||||
|         position: Int | ||||
|         position: Int, | ||||
|         uploadingContributionList: List<Contribution> | ||||
|     ) { | ||||
|         val next = imageLoader.nextActionableImage( | ||||
|             allImages, ioDispatcher, defaultDispatcher, | ||||
|             nextImagePosition | ||||
|             nextImagePosition, uploadingContributionList | ||||
|         ) | ||||
| 
 | ||||
|         // If next actionable image is found, saves it, as the the search for | ||||
|  | @ -328,12 +334,13 @@ class ImageAdapter( | |||
|     /** | ||||
|      * Initialize the data set. | ||||
|      */ | ||||
|     fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) { | ||||
|     fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>, uploadedImages: List<Contribution> = ArrayList()) { | ||||
|         allImages = fixedImages | ||||
|         val oldImageList:ArrayList<Image> = images | ||||
|         val newImageList:ArrayList<Image> = ArrayList(newImages) | ||||
|         actionableImagesMap = emptyMap | ||||
|         alreadyAddedPositions = ArrayList() | ||||
|         uploadingContributionList = uploadedImages | ||||
|         nextImagePosition = 0 | ||||
|         reachedEndOfFolder = false | ||||
|         selectedImages = ArrayList() | ||||
|  | @ -355,15 +362,56 @@ class ImageAdapter( | |||
|     /** | ||||
|      * Refresh the data in the adapter | ||||
|      */ | ||||
|     fun refresh(newImages: List<Image>, fixedImages: List<Image>) { | ||||
|     fun refresh(newImages: List<Image>, fixedImages: List<Image>, uploadingImages: List<Contribution> = ArrayList()) { | ||||
|         numberOfSelectedImagesMarkedAsNotForUpload = 0 | ||||
|         selectedImages.clear() | ||||
|         images.clear() | ||||
|         selectedImages = arrayListOf() | ||||
|         init(newImages, fixedImages, TreeMap()) | ||||
|         init(newImages, fixedImages, TreeMap(),uploadingImages) | ||||
|         notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clear selected images and empty the list. | ||||
|      */ | ||||
|     fun clearSelectedImages(){ | ||||
|         numberOfSelectedImagesMarkedAsNotForUpload = 0 | ||||
|         selectedImages.clear() | ||||
|         selectedImages = arrayListOf() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Remove image from actionable images map. | ||||
|      */ | ||||
|     fun removeImageFromActionableImageMap(image: Image) { | ||||
|         val sharedPreferences: SharedPreferences = | ||||
|             context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0) | ||||
|         val showAlreadyActionedImages = | ||||
|             sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) | ||||
| 
 | ||||
|         if(showAlreadyActionedImages) { | ||||
|             refresh(allImages, allImages, uploadingContributionList) | ||||
|         } else { | ||||
|             val iterator = actionableImagesMap.entries.iterator() | ||||
|             var index = 0 | ||||
| 
 | ||||
|             while (iterator.hasNext()) { | ||||
|                 val entry = iterator.next() | ||||
|                 if (entry.value == image) { | ||||
|                     imagePositionAsPerIncreasingOrder -= 1 | ||||
|                     iterator.remove() | ||||
|                     alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1) | ||||
|                     notifyItemRemoved(index) | ||||
|                     notifyItemRangeChanged(index, itemCount ) | ||||
|                     break | ||||
|                 } | ||||
|                 index++ | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the total number of items in the data set held by the adapter. | ||||
|      * | ||||
|  | @ -407,17 +455,16 @@ class ImageAdapter( | |||
|      */ | ||||
|     class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { | ||||
|         val image: ImageView = itemView.findViewById(R.id.image_thumbnail) | ||||
|         private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) | ||||
|         private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) | ||||
|         private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group) | ||||
|         private val notForUploadGroup: Group = itemView.findViewById(R.id.not_for_upload_group) | ||||
|         private val selectedGroup: Group = itemView.findViewById(R.id.selected_group) | ||||
| 
 | ||||
|         /** | ||||
|          * Item selected view. | ||||
|          */ | ||||
|         fun itemSelected(index: Int) { | ||||
|         fun itemSelected() { | ||||
|             selectedGroup.visibility = View.VISIBLE | ||||
|             selectedNumber.text = index.toString() | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|  | @ -434,6 +481,13 @@ class ImageAdapter( | |||
|             uploadedGroup.visibility = View.VISIBLE | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Item is uploading | ||||
|          */ | ||||
|         fun itemUploading() { | ||||
|             uploadingGroup.visibility = View.VISIBLE | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Item is not for upload view | ||||
|          */ | ||||
|  | @ -452,6 +506,13 @@ class ImageAdapter( | |||
|             return notForUploadGroup.visibility == View.VISIBLE | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Item is not uploading | ||||
|          */ | ||||
|         fun itemNotUploading() { | ||||
|             uploadingGroup.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Item Not Uploaded view. | ||||
|          */ | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper | |||
| import fr.free.nrw.commons.utils.CustomSelectorUtils | ||||
| import kotlinx.coroutines.* | ||||
| import java.io.File | ||||
| import java.lang.Integer.max | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| 
 | ||||
|  | @ -66,6 +67,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|      */ | ||||
|     private lateinit var prefs: SharedPreferences | ||||
| 
 | ||||
|     /** | ||||
|      * Maximum number of images that can be selected. | ||||
|      */ | ||||
|     private val uploadLimit: Int = 20 | ||||
| 
 | ||||
|     /** | ||||
|      * Flag that is marked true when the amount | ||||
|      * of selected images is greater than the upload limit. | ||||
|      */ | ||||
|     private var uploadLimitExceeded: Boolean = false | ||||
| 
 | ||||
|     /** | ||||
|      * Tracks the amount by which the upload limit has been exceeded. | ||||
|      */ | ||||
|     private var uploadLimitExceededBy: Int = 0 | ||||
| 
 | ||||
|     /** | ||||
|      * View Model Factory. | ||||
|      */ | ||||
|  | @ -95,6 +112,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|      */ | ||||
|     var imageFragment: ImageFragment? = null | ||||
| 
 | ||||
|     private var progressDialogText:String="" | ||||
| 
 | ||||
|     /** | ||||
|      * onCreate Activity, sets theme, initialises the view model, setup view. | ||||
|      */ | ||||
|  | @ -140,7 +159,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|                 data!! | ||||
|                     .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!! | ||||
|             val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false) | ||||
|             imageFragment!!.passSelectedImages(selectedImages, shouldRefresh) | ||||
|             imageFragment?.passSelectedImages(selectedImages, shouldRefresh) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -187,17 +206,18 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|             markAsNotForUpload(arrayListOf()) | ||||
|             return | ||||
|         } | ||||
|         var i = 0 | ||||
|         while (i < selectedImages.size) { | ||||
|             val path = selectedImages[i].path | ||||
| 
 | ||||
|         val iterator = selectedImages.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             val image = iterator.next() | ||||
|             val path = image.path | ||||
|             val file = File(path) | ||||
|             if (!file.exists()) { | ||||
|                 selectedImages.removeAt(i) | ||||
|                 i-- | ||||
|                 iterator.remove() | ||||
|             } | ||||
|             i++ | ||||
|         } | ||||
|         markAsNotForUpload(selectedImages) | ||||
|         toolbarBinding.imageLimitError.visibility = View.INVISIBLE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -221,56 +241,63 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|      */ | ||||
|     private fun insertIntoNotForUpload(images: ArrayList<Image>) { | ||||
|         scope.launch { | ||||
|             withContext(Dispatchers.Main) { | ||||
|                 imageFragment?.showMarkUnmarkProgressDialog(text = progressDialogText) | ||||
|             } | ||||
| 
 | ||||
|             var allImagesAlreadyNotForUpload = true | ||||
|             images.forEach { | ||||
|             images.forEach { image -> | ||||
|                 val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||
|                     it.uri, | ||||
|                     image.uri, | ||||
|                     ioDispatcher, | ||||
|                     fileUtilsWrapper, | ||||
|                     contentResolver | ||||
|                 ) | ||||
|                 val exists = notForUploadStatusDao.find(imageSHA1) | ||||
| 
 | ||||
|                 // If image exists in not for upload table make allImagesAlreadyNotForUpload false | ||||
|                 if (exists < 1) { | ||||
|                     allImagesAlreadyNotForUpload = false | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // if all images is not already marked as not for upload, insert all images in | ||||
|             // not for upload table | ||||
|             if (!allImagesAlreadyNotForUpload) { | ||||
|                 images.forEach { | ||||
|                 // Insert or delete images as necessary, but the UI updates should be posted back to the main thread | ||||
|                 images.forEach { image -> | ||||
|                     val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||
|                         it.uri, | ||||
|                         image.uri, | ||||
|                         ioDispatcher, | ||||
|                         fileUtilsWrapper, | ||||
|                         contentResolver | ||||
|                     ) | ||||
|                     notForUploadStatusDao.insert( | ||||
|                         NotForUploadStatus( | ||||
|                             imageSHA1 | ||||
|                         ) | ||||
|                     ) | ||||
|                     notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1)) | ||||
|                 } | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     images.forEach { image -> | ||||
|                         imageFragment?.removeImage(image) | ||||
|                     } | ||||
|                     imageFragment?.clearSelectedImages() | ||||
|                 } | ||||
| 
 | ||||
|                 // if all images is already marked as not for upload, delete all images from | ||||
|                 // not for upload table | ||||
|             } else { | ||||
|                 images.forEach { | ||||
|                 images.forEach { image -> | ||||
|                     val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||
|                         it.uri, | ||||
|                         image.uri, | ||||
|                         ioDispatcher, | ||||
|                         fileUtilsWrapper, | ||||
|                         contentResolver | ||||
|                     ) | ||||
|                     notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1) | ||||
|                 } | ||||
| 
 | ||||
|                 withContext(Dispatchers.Main) { | ||||
|                     imageFragment?.refresh() | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             imageFragment!!.refresh() | ||||
|             val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout) | ||||
|             bottomLayout.visibility = View.GONE | ||||
|             withContext(Dispatchers.Main) { | ||||
|                 imageFragment?.dismissMarkUnmarkProgressDialog() | ||||
|                 val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout) | ||||
|                 bottomLayout.visibility = View.GONE | ||||
|                 changeTitle(bucketName, 0) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -284,10 +311,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|     /** | ||||
|      * Change the title of the toolbar. | ||||
|      */ | ||||
|     private fun changeTitle(title: String) { | ||||
|         val titleText = findViewById<TextView>(R.id.title) | ||||
|         if (titleText != null) { | ||||
|             titleText.text = title | ||||
|     private fun changeTitle(title: String, selectedImageCount:Int) { | ||||
|         if (title.isNotEmpty()){ | ||||
|             val titleText = findViewById<TextView>(R.id.title) | ||||
|             var titleWithAppendedImageCount = title | ||||
|             if (selectedImageCount > 0) { | ||||
|                 titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix,  | ||||
|                     selectedImageCount, selectedImageCount)})" | ||||
|             } | ||||
|             if (titleText != null) { | ||||
|                 titleText.text = titleWithAppendedImageCount | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -297,6 +331,10 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|     private fun setUpToolbar() { | ||||
|         val back: ImageButton = findViewById(R.id.back) | ||||
|         back.setOnClickListener { onBackPressed() } | ||||
| 
 | ||||
|         val limitError: ImageButton = findViewById(R.id.image_limit_error) | ||||
|         limitError.visibility = View.INVISIBLE | ||||
|         limitError.setOnClickListener { displayUploadLimitWarning() } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -308,7 +346,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|             .addToBackStack(null) | ||||
|             .commit() | ||||
| 
 | ||||
|         changeTitle(folderName) | ||||
|         changeTitle(folderName, 0) | ||||
| 
 | ||||
|         bucketId = folderId | ||||
|         bucketName = folderName | ||||
|  | @ -323,8 +361,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|         selectedNotForUploadImages: Int | ||||
|     ) { | ||||
|         viewModel.selectedImages.value = selectedImages | ||||
|         changeTitle(bucketName, selectedImages.size) | ||||
| 
 | ||||
|         if (selectedNotForUploadImages > 0) { | ||||
|         uploadLimitExceeded = selectedImages.size > uploadLimit | ||||
|         uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0) | ||||
| 
 | ||||
|         if (uploadLimitExceeded && selectedNotForUploadImages == 0) { | ||||
|             toolbarBinding.imageLimitError.visibility = View.VISIBLE | ||||
|             bottomSheetBinding.upload.text = resources.getString( | ||||
|                 R.string.custom_selector_button_limit_text, uploadLimit) | ||||
|         } else { | ||||
|             toolbarBinding.imageLimitError.visibility = View.INVISIBLE | ||||
|             bottomSheetBinding.upload.text = resources.getString(R.string.upload) | ||||
|         } | ||||
| 
 | ||||
|         if (uploadLimitExceeded || selectedNotForUploadImages > 0) { | ||||
|             bottomSheetBinding.upload.isEnabled = false | ||||
|             bottomSheetBinding.upload.alpha = 0.5f | ||||
|         } else { | ||||
|  | @ -334,8 +385,14 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
| 
 | ||||
|         bottomSheetBinding.notForUpload.text = | ||||
|             when (selectedImages.size == selectedNotForUploadImages) { | ||||
|                 true -> getString(R.string.unmark_as_not_for_upload) | ||||
|                 else -> getString(R.string.mark_as_not_for_upload) | ||||
|                 true -> { | ||||
|                     progressDialogText=getString(R.string.unmarking_as_not_for_upload) | ||||
|                     getString(R.string.unmark_as_not_for_upload) | ||||
|                 } | ||||
|                 else -> { | ||||
|                     progressDialogText=getString(R.string.marking_as_not_for_upload) | ||||
|                     getString(R.string.mark_as_not_for_upload) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout) | ||||
|  | @ -366,22 +423,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|      * Get the selected images. Remove any non existent file, forward the data to finish selector. | ||||
|      */ | ||||
|     fun onDone() { | ||||
|         val selectedImages = viewModel.selectedImages.value | ||||
|         if (selectedImages.isNullOrEmpty()) { | ||||
|             finishPickImages(arrayListOf()) | ||||
|             return | ||||
|         } | ||||
|         var i = 0 | ||||
|         while (i < selectedImages.size) { | ||||
|             val path = selectedImages[i].path | ||||
|             val file = File(path) | ||||
|             if (!file.exists()) { | ||||
|                 selectedImages.removeAt(i) | ||||
|                 i-- | ||||
|             val selectedImages = viewModel.selectedImages.value | ||||
|             if (selectedImages.isNullOrEmpty()) { | ||||
|                 finishPickImages(arrayListOf()) | ||||
|                 return | ||||
|             } | ||||
|             i++ | ||||
|         } | ||||
|         finishPickImages(selectedImages) | ||||
|             var i = 0 | ||||
|             while (i < selectedImages.size) { | ||||
|                 val path = selectedImages[i].path | ||||
|                 val file = File(path) | ||||
|                 if (!file.exists()) { | ||||
|                     selectedImages.removeAt(i) | ||||
|                     i-- | ||||
|                 } | ||||
|                 i++ | ||||
|             } | ||||
|             finishPickImages(selectedImages) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -404,10 +461,24 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | |||
|         val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) | ||||
|         if (fragment != null && fragment is FolderFragment) { | ||||
|             isImageFragmentOpen = false | ||||
|             changeTitle(getString(R.string.custom_selector_title)) | ||||
|             changeTitle(getString(R.string.custom_selector_title), 0) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Displays a dialog explaining the upload limit warning. | ||||
|      */ | ||||
|     private fun displayUploadLimitWarning() { | ||||
|         val dialog = Dialog(this) | ||||
|         dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) | ||||
|         dialog.setContentView(R.layout.custom_selector_limit_dialog) | ||||
|         (dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener() | ||||
|         { dialog.dismiss() } | ||||
|         (dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString( | ||||
|             R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy) | ||||
|         dialog.show() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * On activity destroy | ||||
|      * If image fragment is open, overwrite its attributes otherwise discard the values. | ||||
|  |  | |||
|  | @ -93,7 +93,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { | |||
|      */ | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||
|         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) | ||||
|         folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) | ||||
|         folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) | ||||
|         gridLayoutManager = GridLayoutManager(context, columnCount()) | ||||
|         selectorRV = binding?.selectorRv | ||||
|         loader = binding?.loader | ||||
|  |  | |||
|  | @ -11,7 +11,9 @@ import kotlinx.coroutines.Dispatchers | |||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import java.io.File | ||||
| import java.util.* | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| /** | ||||
|  | @ -90,6 +92,12 @@ class ImageFileLoader(val context: Context) : CoroutineScope{ | |||
|                     } | ||||
| 
 | ||||
|                 if (file != null && file.exists() && name != null && path != null && bucketName != null) { | ||||
|                     val extension = path.substringAfterLast(".", "") | ||||
|                     // 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")) { | ||||
|                         continue | ||||
|                     } | ||||
| 
 | ||||
|                     val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) | ||||
| 
 | ||||
|                     val calendar = Calendar.getInstance() | ||||
|  |  | |||
|  | @ -9,37 +9,41 @@ import android.view.View | |||
| import android.view.ViewGroup | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.Switch | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.lifecycle.ViewModelProvider | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.ContributionDao | ||||
| import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao | ||||
| import fr.free.nrw.commons.customselector.database.UploadedStatusDao | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper | ||||
| import fr.free.nrw.commons.customselector.listeners.PassDataListener | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY | ||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY | ||||
| import fr.free.nrw.commons.customselector.listeners.ImageSelectListener | ||||
| import fr.free.nrw.commons.customselector.listeners.PassDataListener | ||||
| import fr.free.nrw.commons.customselector.listeners.RefreshUIListener | ||||
| import fr.free.nrw.commons.customselector.model.CallbackStatus | ||||
| import fr.free.nrw.commons.customselector.model.Image | ||||
| import fr.free.nrw.commons.customselector.model.Result | ||||
| import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter | ||||
| import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding | ||||
| import fr.free.nrw.commons.databinding.ProgressDialogBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import fr.free.nrw.commons.upload.FileProcessor | ||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import java.util.* | ||||
| import javax.inject.Inject | ||||
| import kotlin.collections.ArrayList | ||||
| 
 | ||||
| /** | ||||
|  * Custom Selector Image Fragment. | ||||
|  */ | ||||
| class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener { | ||||
| class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener { | ||||
| 
 | ||||
|     private var _binding: FragmentCustomSelectorBinding? = null | ||||
|     private val binding get() = _binding | ||||
|  | @ -57,7 +61,7 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|     /** | ||||
|      * View model for images. | ||||
|      */ | ||||
|     private var  viewModel: CustomSelectorViewModel? = null | ||||
|     private var viewModel: CustomSelectorViewModel? = null | ||||
| 
 | ||||
|     /** | ||||
|      * View Elements. | ||||
|  | @ -99,6 +103,10 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|      */ | ||||
|     private var progressLayout: ConstraintLayout? = null | ||||
| 
 | ||||
|     private lateinit var progressDialog: AlertDialog | ||||
|     private lateinit var progressDialogLayout: ProgressDialogBinding | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * NotForUploadStatus Dao class for database operations | ||||
|      */ | ||||
|  | @ -129,6 +137,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|     @Inject | ||||
|     lateinit var mediaClient: MediaClient | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var contributionDao: ContributionDao | ||||
| 
 | ||||
|     companion object { | ||||
| 
 | ||||
|         /** | ||||
|  | @ -163,7 +174,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|         super.onCreate(savedInstanceState) | ||||
|         bucketId = arguments?.getLong(BUCKET_ID) | ||||
|         lastItemId = arguments?.getLong(LAST_ITEM_ID, 0) | ||||
|         viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) | ||||
|         viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get( | ||||
|             CustomSelectorViewModel::class.java | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -171,17 +184,22 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|      * Init imageAdapter, gridLayoutManger. | ||||
|      * SetUp recycler view. | ||||
|      */ | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) | ||||
|         imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) | ||||
|         gridLayoutManager = GridLayoutManager(context,getSpanCount()) | ||||
|         with(binding?.selectorRv){ | ||||
|         imageAdapter = | ||||
|             ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) | ||||
|         gridLayoutManager = GridLayoutManager(context, getSpanCount()) | ||||
|         with(binding?.selectorRv) { | ||||
|             this?.layoutManager = gridLayoutManager | ||||
|             this?.setHasFixedSize(true) | ||||
|             this?.adapter = imageAdapter | ||||
|         } | ||||
| 
 | ||||
|         viewModel?.result?.observe(viewLifecycleOwner, Observer{ | ||||
|         viewModel?.result?.observe(viewLifecycleOwner, Observer { | ||||
|             handleResult(it) | ||||
|         }) | ||||
| 
 | ||||
|  | @ -194,9 +212,16 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
| 
 | ||||
|         val sharedPreferences: SharedPreferences = | ||||
|             requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE) | ||||
|         showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) | ||||
|         showAlreadyActionedImages = | ||||
|             sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) | ||||
|         switch?.isChecked = showAlreadyActionedImages | ||||
| 
 | ||||
|         val builder = AlertDialog.Builder(requireActivity()) | ||||
|         builder.setCancelable(false) | ||||
|         progressDialogLayout = ProgressDialogBinding.inflate(layoutInflater, container, false) | ||||
|         builder.setView(progressDialogLayout.root) | ||||
|         progressDialog = builder.create() | ||||
| 
 | ||||
|         return binding?.root | ||||
|     } | ||||
| 
 | ||||
|  | @ -217,7 +242,8 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|             editor.apply() | ||||
|         } | ||||
| 
 | ||||
|         imageAdapter.init(allImages, allImages, TreeMap()) | ||||
|         val uploadingContributions = getUploadingContributions() | ||||
|         imageAdapter.init(allImages, allImages, TreeMap(), uploadingContributions) | ||||
|         imageAdapter.notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|  | @ -236,13 +262,15 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|     /** | ||||
|      * Handle view model result. | ||||
|      */ | ||||
|     private fun handleResult(result:Result){ | ||||
|         if(result.status is CallbackStatus.SUCCESS){ | ||||
|     private fun handleResult(result: Result) { | ||||
|         if (result.status is CallbackStatus.SUCCESS) { | ||||
|             val images = result.images | ||||
|             if(images.isNotEmpty()) { | ||||
| 
 | ||||
|             val uploadingContributions = getUploadingContributions() | ||||
|             if (images.isNotEmpty()) { | ||||
|                 filteredImages = ImageHelper.filterImages(images, bucketId) | ||||
|                 allImages = ArrayList(filteredImages) | ||||
|                 imageAdapter.init(filteredImages, allImages, TreeMap()) | ||||
|                 imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) | ||||
|                 selectorRV?.let { | ||||
|                     it.visibility = View.VISIBLE | ||||
|                     lastItemId?.let { pos -> | ||||
|  | @ -250,18 +278,18 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|                             .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else{ | ||||
|             } else { | ||||
|                 binding?.emptyText?.let { | ||||
|                     it.visibility = View.VISIBLE | ||||
|                 } | ||||
|                 selectorRV?.let{ | ||||
|                 selectorRV?.let { | ||||
|                     it.visibility = View.GONE | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         loader?.let { | ||||
|             it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE | ||||
|             it.visibility = | ||||
|                 if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -317,19 +345,61 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | |||
|     } | ||||
| 
 | ||||
|     override fun refresh() { | ||||
|         imageAdapter.refresh(filteredImages, allImages) | ||||
|         imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes the image from the actionable image map | ||||
|      */ | ||||
|     fun removeImage(image : Image){ | ||||
|         imageAdapter.removeImageFromActionableImageMap(image) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clears the selected images | ||||
|      */ | ||||
|     fun clearSelectedImages() { | ||||
|         imageAdapter.clearSelectedImages() | ||||
|     } | ||||
|     /** | ||||
|      * Passes selected images and other information from Activity to Fragment and connects it with | ||||
|      * the adapter | ||||
|      */ | ||||
|     override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean){ | ||||
|     override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) { | ||||
|         imageAdapter.setSelectedImages(selectedImages) | ||||
| 
 | ||||
|         val uploadingContributions = getUploadingContributions() | ||||
| 
 | ||||
|         if (!showAlreadyActionedImages && shouldRefresh) { | ||||
|             imageAdapter.init(filteredImages, allImages, TreeMap()) | ||||
|             imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) | ||||
|             imageAdapter.setSelectedImages(selectedImages) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows mark/unmark progress dialog | ||||
|      */ | ||||
|     fun showMarkUnmarkProgressDialog(text: String) { | ||||
|         if (!progressDialog.isShowing) { | ||||
|             progressDialogLayout.progressDialogText.text = text | ||||
|             progressDialog.show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Dismisses mark/unmark progress dialog | ||||
|      */ | ||||
|     fun dismissMarkUnmarkProgressDialog() { | ||||
|         if (progressDialog.isShowing) { | ||||
|             progressDialog.dismiss() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getUploadingContributions(): List<Contribution> { | ||||
| 
 | ||||
|         return  contributionDao.getContribution( | ||||
|             listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED) | ||||
|         )?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList() | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector | |||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.net.Uri | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao | ||||
| import fr.free.nrw.commons.customselector.database.UploadedStatus | ||||
| import fr.free.nrw.commons.customselector.database.UploadedStatusDao | ||||
|  | @ -75,7 +76,8 @@ class ImageLoader @Inject constructor( | |||
|         holder: ImageViewHolder, | ||||
|         image: Image, | ||||
|         ioDispatcher: CoroutineDispatcher, | ||||
|         defaultDispatcher: CoroutineDispatcher | ||||
|         defaultDispatcher: CoroutineDispatcher, | ||||
|         uploadedContributionsList : List<Contribution> | ||||
|     ) { | ||||
| 
 | ||||
|         /** | ||||
|  | @ -84,6 +86,7 @@ class ImageLoader @Inject constructor( | |||
|         mapHolderImage[holder] = image | ||||
|         holder.itemNotUploaded() | ||||
|         holder.itemForUpload() | ||||
|         holder.itemNotUploading() | ||||
| 
 | ||||
|         scope.launch { | ||||
|             var result: Result = Result.NOTFOUND | ||||
|  | @ -214,6 +217,17 @@ class ImageLoader @Inject constructor( | |||
|                     holder.itemNotForUpload() | ||||
|                 } else holder.itemForUpload() | ||||
|             } | ||||
| 
 | ||||
|             if (uploadedContributionsList.isNotEmpty()) { | ||||
|                 for (contribution in uploadedContributionsList ) { | ||||
|                     if (contribution.contentUri == image.uri && showAlreadyActionedImages) { | ||||
|                         holder.itemUploading() | ||||
|                         break | ||||
|                     } else { | ||||
|                         holder.itemNotUploading() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -223,17 +237,22 @@ class ImageLoader @Inject constructor( | |||
|     suspend fun nextActionableImage( | ||||
|         allImages: List<Image>, ioDispatcher: CoroutineDispatcher, | ||||
|         defaultDispatcher: CoroutineDispatcher, | ||||
|         nextImagePosition: Int | ||||
|         nextImagePosition: Int, | ||||
|         currentlyUploadingImages: List<Contribution> | ||||
|     ): Int { | ||||
|         var next: Int | ||||
| 
 | ||||
|         // Traversing from given position to the end | ||||
|         for (i in nextImagePosition until allImages.size){ | ||||
|             val it = allImages[i] | ||||
|             val imageSHA1: String = when (mapImageSHA1[it.uri] != null) { | ||||
|                 true -> mapImageSHA1[it.uri]!! | ||||
|             val currentImage = allImages[i] | ||||
| 
 | ||||
|             if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) { | ||||
|                 continue // Skip this image as it's currently being uploaded | ||||
|             } | ||||
| 
 | ||||
|             val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) { | ||||
|                 true -> mapImageSHA1[currentImage.uri]!! | ||||
|                 else -> CustomSelectorUtils.getImageSHA1( | ||||
|                     it.uri, | ||||
|                     currentImage.uri, | ||||
|                     ioDispatcher, | ||||
|                     fileUtilsWrapper, | ||||
|                     context.contentResolver | ||||
|  | @ -253,7 +272,7 @@ class ImageLoader @Inject constructor( | |||
|                 // If the image is not present in the already uploaded table, checks for its | ||||
|                 // modified SHA1 in already uploaded table | ||||
|                 if (next <= 0) { | ||||
|                     val modifiedImageSha1 = getSHA1(it, defaultDispatcher) | ||||
|                     val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher) | ||||
|                     next = uploadedStatusDao.findByModifiedImageSHA1( | ||||
|                         modifiedImageSha1, | ||||
|                         true | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ import androidx.room.TypeConverters | |||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.ContributionDao | ||||
| import fr.free.nrw.commons.customselector.database.* | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.nearby.PlaceDao | ||||
| import fr.free.nrw.commons.review.ReviewDao | ||||
| import fr.free.nrw.commons.review.ReviewEntity | ||||
| import fr.free.nrw.commons.upload.depicts.Depicts | ||||
|  | @ -15,10 +17,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao | |||
|  * The database for accessing the respective DAOs | ||||
|  * | ||||
|  */ | ||||
| @Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 15, exportSchema = false) | ||||
| @Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], version = 18, exportSchema = false) | ||||
| @TypeConverters(Converters::class) | ||||
| abstract class AppDatabase : RoomDatabase() { | ||||
|     abstract fun contributionDao(): ContributionDao | ||||
|     abstract fun PlaceDao(): PlaceDao | ||||
|     abstract fun DepictsDao(): DepictsDao; | ||||
|     abstract fun UploadedStatusDao(): UploadedStatusDao; | ||||
|     abstract fun NotForUploadStatusDao(): NotForUploadStatusDao | ||||
|  |  | |||
|  | @ -8,8 +8,10 @@ import fr.free.nrw.commons.CommonsApplication; | |||
| import fr.free.nrw.commons.contributions.ChunkInfo; | ||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.nearby.Sitelinks; | ||||
| import fr.free.nrw.commons.upload.WikidataPlace; | ||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | ||||
| import java.lang.reflect.Type; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | @ -134,6 +136,18 @@ public class Converters { | |||
|         return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {}); | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     public static Sitelinks sitelinksFromString(String value) { | ||||
|         Type type = new TypeToken<Sitelinks>() {}.getType(); | ||||
|         return new Gson().fromJson(value, type); | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     public static String fromSitelinks(Sitelinks sitelinks) { | ||||
|         Gson gson = new Gson(); | ||||
|         return gson.toJson(sitelinks); | ||||
|     } | ||||
| 
 | ||||
|     private static String writeObjectToString(Object object) { | ||||
|         return object == null ? null : getGson().toJson(object); | ||||
|     } | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import fr.free.nrw.commons.BuildConfig; | |||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.actions.PageEditClient; | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; | ||||
| import fr.free.nrw.commons.notification.NotificationHelper; | ||||
| import fr.free.nrw.commons.review.ReviewController; | ||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | ||||
|  | @ -66,7 +67,13 @@ public class DeleteHelper { | |||
| 
 | ||||
|         return delete(media, reason) | ||||
|                 .flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result))) | ||||
|                 .firstOrError(); | ||||
|                 .firstOrError() | ||||
|                 .onErrorResumeNext(throwable -> { | ||||
|                     if (throwable instanceof InvalidLoginTokenException) { | ||||
|                         return Single.error(throwable); | ||||
|                     } | ||||
|                     return Single.error(throwable); | ||||
|                 }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -104,22 +111,30 @@ public class DeleteHelper { | |||
|         } | ||||
| 
 | ||||
|         return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary) | ||||
|                 .flatMap(result -> { | ||||
|                     if (result) { | ||||
|                         return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); | ||||
|                     } | ||||
|                     throw new RuntimeException("Failed to nominate for deletion"); | ||||
|                 }).flatMap(result -> { | ||||
|                     if (result) { | ||||
|                         return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary); | ||||
|                     } | ||||
|                     throw new RuntimeException("Failed to nominate for deletion"); | ||||
|                 }).flatMap(result -> { | ||||
|                     if (result) { | ||||
|                         return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary); | ||||
|                     } | ||||
|                     throw new RuntimeException("Failed to nominate for deletion"); | ||||
|                 }); | ||||
|             .onErrorResumeNext(throwable -> { | ||||
|                 if (throwable instanceof InvalidLoginTokenException) { | ||||
|                     return Observable.error(throwable); | ||||
|                 } | ||||
|                 return Observable.error(throwable); | ||||
|             }) | ||||
|             .flatMap(result -> { | ||||
|                 if (result) { | ||||
|                     return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); | ||||
|                 } | ||||
|                 return Observable.error(new RuntimeException("Failed to nominate for deletion")); | ||||
|             }) | ||||
|             .flatMap(result -> { | ||||
|                 if (result) { | ||||
|                     return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary); | ||||
|                 } | ||||
|                 return Observable.error(new RuntimeException("Failed to nominate for deletion")); | ||||
|             }) | ||||
|             .flatMap(result -> { | ||||
|                 if (result) { | ||||
|                     return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary); | ||||
|                 } | ||||
|                 return Observable.error(new RuntimeException("Failed to nominate for deletion")); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     private boolean showDeletionNotification(Context context, Media media, boolean result) { | ||||
|  | @ -205,6 +220,8 @@ public class DeleteHelper { | |||
|         }); | ||||
| 
 | ||||
|         alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> { | ||||
|             reviewCallback.disableButtons(); | ||||
| 
 | ||||
| 
 | ||||
|             String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " "; | ||||
| 
 | ||||
|  | @ -224,13 +241,15 @@ public class DeleteHelper { | |||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(aBoolean -> { | ||||
|                     if (aBoolean) { | ||||
|                         reviewCallback.onSuccess(); | ||||
|                     reviewCallback.onSuccess(); | ||||
|                 }, throwable -> { | ||||
|                     if (throwable instanceof InvalidLoginTokenException) { | ||||
|                         reviewCallback.onTokenException((InvalidLoginTokenException) throwable); | ||||
|                     } else { | ||||
|                         reviewCallback.onFailure(); | ||||
|                     } | ||||
|                     reviewCallback.enableButtons(); | ||||
|                 }); | ||||
| 
 | ||||
|         }); | ||||
|         alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure()); | ||||
|         d = alert.create(); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package fr.free.nrw.commons.delete; | |||
| 
 | ||||
| import android.content.Context; | ||||
| 
 | ||||
| import org.wikipedia.util.DateUtil; | ||||
| import fr.free.nrw.commons.utils.DateUtil; | ||||
| 
 | ||||
| import java.util.Date; | ||||
| import java.util.Locale; | ||||
|  |  | |||
|  | @ -1,16 +1,21 @@ | |||
| package fr.free.nrw.commons.description | ||||
| 
 | ||||
| 
 | ||||
| import android.app.ProgressDialog | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.os.Parcelable | ||||
| import android.speech.RecognizerIntent | ||||
| import android.view.View | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||
| import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding | ||||
| import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION | ||||
| import fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT | ||||
| import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||
| import fr.free.nrw.commons.settings.Prefs | ||||
|  | @ -18,8 +23,13 @@ import fr.free.nrw.commons.theme.BaseActivity | |||
| import fr.free.nrw.commons.upload.UploadMediaDetail | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.functions.Consumer | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Activity for populating and editing existing description and caption | ||||
|  */ | ||||
|  | @ -40,6 +50,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
|      */ | ||||
|     var wikiText: String? = null | ||||
| 
 | ||||
|     /** | ||||
|      * Media object | ||||
|      */ | ||||
|     var media: Media? = null | ||||
| 
 | ||||
|     /** | ||||
|      * Saved language | ||||
|      */ | ||||
|  | @ -55,6 +70,15 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
| 
 | ||||
|     private lateinit var binding: ActivityDescriptionEditBinding | ||||
| 
 | ||||
|     private val REQUEST_CODE_FOR_VOICE_INPUT = 1213 | ||||
| 
 | ||||
|     private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null | ||||
| 
 | ||||
|     @Inject lateinit var descriptionEditHelper: DescriptionEditHelper | ||||
| 
 | ||||
|     @Inject lateinit var sessionManager: SessionManager | ||||
| 
 | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| 
 | ||||
|  | @ -62,13 +86,21 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
|         setContentView(binding.root) | ||||
| 
 | ||||
|         val bundle = intent.extras | ||||
|         val descriptionAndCaptions: ArrayList<UploadMediaDetail> = | ||||
|             bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!! | ||||
|         wikiText = bundle.getString(WIKITEXT) | ||||
|         savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!! | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             descriptionAndCaptions = savedInstanceState.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION) | ||||
|             wikiText = savedInstanceState.getString(WIKITEXT) | ||||
|             savedLanguageValue = savedInstanceState.getString(Prefs.DESCRIPTION_LANGUAGE)!! | ||||
|             media = savedInstanceState.getParcelable("media") | ||||
|         } else { | ||||
|             descriptionAndCaptions = | ||||
|                 bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!! | ||||
|             wikiText = bundle.getString(WIKITEXT) | ||||
|             savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!! | ||||
|             media = bundle.getParcelable("media") | ||||
|         } | ||||
|         initRecyclerView(descriptionAndCaptions) | ||||
| 
 | ||||
|         binding.btnAddDescription.setOnClickListener(::onButtonAddDescriptionClicked) | ||||
|         binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked) | ||||
|         binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked) | ||||
|     } | ||||
|  | @ -78,7 +110,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
|      * @param descriptionAndCaptions list of description and caption | ||||
|      */ | ||||
|     private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { | ||||
|         uploadMediaDetailAdapter = UploadMediaDetailAdapter( | ||||
|         uploadMediaDetailAdapter = UploadMediaDetailAdapter(this, | ||||
|             savedLanguageValue, descriptionAndCaptions, recentLanguagesDao) | ||||
|         uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> | ||||
|             showInfoAlert( | ||||
|  | @ -107,17 +139,20 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
| 
 | ||||
|     override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {} | ||||
| 
 | ||||
|     private fun onBackButtonClicked(view: View) { | ||||
|         onBackPressed() | ||||
|     } | ||||
| 
 | ||||
|     private fun onButtonAddDescriptionClicked(view: View) { | ||||
|     /** | ||||
|      * Adds new language item to RecyclerView | ||||
|      */ | ||||
|     override fun addLanguage() { | ||||
|         val uploadMediaDetail = UploadMediaDetail() | ||||
|         uploadMediaDetail.isManuallyAdded = true //This was manually added by the user | ||||
|         uploadMediaDetailAdapter.addDescription(uploadMediaDetail) | ||||
|         rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) | ||||
|     } | ||||
| 
 | ||||
|     private fun onBackButtonClicked(view: View) { | ||||
|        onBackPressedDispatcher.onBackPressed() | ||||
|     } | ||||
| 
 | ||||
|     private fun onSubmitButtonClicked(view: View) { | ||||
|         showLoggingProgressBar() | ||||
|         val uploadMediaDetails = uploadMediaDetailAdapter.items | ||||
|  | @ -151,22 +186,85 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
|                     buffer.append(uploadDetails.languageCode) | ||||
|                     buffer.append("|1=") | ||||
|                     buffer.append(uploadDetails.descriptionText) | ||||
|                     buffer.append("}}, ") | ||||
|                     buffer.append("}}") | ||||
|                 } | ||||
|             } | ||||
|             buffer.replace(", $".toRegex(), "") | ||||
|             buffer.append(descriptionEnd) | ||||
|         } | ||||
|         val returningIntent = Intent() | ||||
|         returningIntent.putExtra(UPDATED_WIKITEXT, buffer.toString()) | ||||
|         returningIntent.putParcelableArrayListExtra( | ||||
|             LIST_OF_DESCRIPTION_AND_CAPTION, | ||||
|             uploadMediaDetails as ArrayList<out Parcelable?> | ||||
|         ) | ||||
|         setResult(RESULT_OK, returningIntent) | ||||
|         editDescription(media!!, buffer.toString(), uploadMediaDetails as ArrayList<UploadMediaDetail>) | ||||
| 
 | ||||
|         finish() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Edits description and caption | ||||
|      * @param media media object | ||||
|      * @param updatedWikiText updated wiki text | ||||
|      * @param uploadMediaDetails descriptions and captions | ||||
|      */ | ||||
|     private fun editDescription(media : Media, updatedWikiText : String, uploadMediaDetails : ArrayList<UploadMediaDetail>){ | ||||
| 
 | ||||
|         try { | ||||
|             descriptionEditHelper?.addDescription( | ||||
|                 applicationContext, media, | ||||
|                 updatedWikiText | ||||
|             ) | ||||
|                 ?.subscribeOn(Schedulers.io()) | ||||
|                 ?.observeOn(AndroidSchedulers.mainThread()) | ||||
|                 ?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })?.let { | ||||
|                     compositeDisposable.add( | ||||
|                         it | ||||
|                     ) | ||||
|                 } | ||||
|         } catch (e : InvalidLoginTokenException) { | ||||
|             val username: String? = sessionManager?.userName | ||||
|             val logoutListener = CommonsApplication.BaseLogoutListener( | ||||
|                 this, | ||||
|                 getString(R.string.invalid_login_message), | ||||
|                 username | ||||
|             ) | ||||
| 
 | ||||
|             val commonsApplication = CommonsApplication.getInstance() | ||||
|             if (commonsApplication != null ){ | ||||
|                 commonsApplication.clearApplicationData(this,logoutListener) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         val updatedCaptions = LinkedHashMap<String, String>() | ||||
|         for (mediaDetail in uploadMediaDetails) { | ||||
|             try { | ||||
|                 compositeDisposable.add( | ||||
|                     descriptionEditHelper!!.addCaption( | ||||
|                         applicationContext, media, | ||||
|                         mediaDetail.languageCode, mediaDetail.captionText | ||||
|                     ) | ||||
|                         .subscribeOn(Schedulers.io()) | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe { s: Boolean? -> | ||||
|                             updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText | ||||
|                             media.captions = updatedCaptions | ||||
|                             Timber.d("Caption is added.") | ||||
|                         }) | ||||
|             } | ||||
|             catch (e : InvalidLoginTokenException) { | ||||
|                 val username = sessionManager.userName | ||||
|                 val logoutListener = CommonsApplication.BaseLogoutListener( | ||||
|                     this, | ||||
|                     getString(R.string.invalid_login_message), | ||||
|                     username | ||||
|                 ) | ||||
| 
 | ||||
|                 val commonsApplication = CommonsApplication.getInstance() | ||||
|                 if (commonsApplication != null ){ | ||||
|                     commonsApplication.clearApplicationData(this,logoutListener) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showLoggingProgressBar() { | ||||
|         progressDialog = ProgressDialog(this) | ||||
|         progressDialog!!.isIndeterminate = true | ||||
|  | @ -175,4 +273,24 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | |||
|         progressDialog!!.setCanceledOnTouchOutside(false) | ||||
|         progressDialog!!.show() | ||||
|     } | ||||
| 
 | ||||
|     override | ||||
|     fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
|         if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { | ||||
|             if (resultCode == RESULT_OK && data != null) { | ||||
|                 val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS ) | ||||
|                 uploadMediaDetailAdapter.handleSpeechResult(result!![0]) } | ||||
|             else { Timber.e("Error %s", resultCode) } | ||||
|         } | ||||
|     } | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
| 
 | ||||
|         outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>) | ||||
|         outState.putString(WIKITEXT, wikiText) | ||||
|         outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue) | ||||
|         //save Media | ||||
|         outState.putParcelable("media", media) | ||||
|     } | ||||
| } | ||||
|  | @ -14,6 +14,7 @@ import fr.free.nrw.commons.description.DescriptionEditActivity; | |||
| import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; | ||||
| import fr.free.nrw.commons.explore.SearchActivity; | ||||
| import fr.free.nrw.commons.media.ZoomableActivity; | ||||
| import fr.free.nrw.commons.nearby.WikidataFeedback; | ||||
| import fr.free.nrw.commons.notification.NotificationActivity; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.review.ReviewActivity; | ||||
|  | @ -79,4 +80,7 @@ public abstract class ActivityBuilderModule { | |||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract ZoomableActivity bindZoomableActivity(); | ||||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract WikidataFeedback bindWikiFeedback(); | ||||
| } | ||||
|  |  | |||
|  | @ -2,10 +2,11 @@ package fr.free.nrw.commons.di; | |||
| 
 | ||||
| import com.google.gson.Gson; | ||||
| 
 | ||||
| import fr.free.nrw.commons.actions.PageEditClient; | ||||
| import fr.free.nrw.commons.explore.categories.CategoriesModule; | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; | ||||
| import fr.free.nrw.commons.navtab.NavTabLayout; | ||||
| import fr.free.nrw.commons.nearby.NearbyController; | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker; | ||||
| import javax.inject.Singleton; | ||||
| 
 | ||||
|  | @ -69,6 +70,9 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application | |||
| 
 | ||||
|     void inject(PicOfDayAppWidget picOfDayAppWidget); | ||||
| 
 | ||||
|     @Singleton | ||||
|     void inject(NearbyController nearbyController); | ||||
| 
 | ||||
|     Gson gson(); | ||||
| 
 | ||||
|     @Component.Builder | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import fr.free.nrw.commons.data.DBOpenHelper; | |||
| import fr.free.nrw.commons.db.AppDatabase; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.nearby.PlaceDao; | ||||
| import fr.free.nrw.commons.review.ReviewDao; | ||||
| import fr.free.nrw.commons.settings.Prefs; | ||||
| import fr.free.nrw.commons.upload.UploadController; | ||||
|  | @ -41,7 +42,6 @@ import java.util.Map; | |||
| import java.util.Objects; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
| import org.wikipedia.AppAdapter; | ||||
| 
 | ||||
| /** | ||||
|  * The Dependency Provider class for Commons Android. | ||||
|  | @ -257,8 +257,8 @@ public class CommonsApplicationModule { | |||
| 
 | ||||
|     @Named("username") | ||||
|     @Provides | ||||
|     public String provideLoggedInUsername() { | ||||
|         return Objects.toString(AppAdapter.get().getUserName(), ""); | ||||
|     public String provideLoggedInUsername(SessionManager sessionManager) { | ||||
|         return Objects.toString(sessionManager.getUserName(), ""); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|  | @ -276,6 +276,11 @@ public class CommonsApplicationModule { | |||
|         return appDatabase.contributionDao(); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     public PlaceDao providesPlaceDao(AppDatabase appDatabase) { | ||||
|         return appDatabase.PlaceDao(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the reference of DepictsDao class. | ||||
|      */ | ||||
|  |  | |||
|  | @ -7,8 +7,16 @@ import dagger.Module; | |||
| import dagger.Provides; | ||||
| import fr.free.nrw.commons.BetaConstants; | ||||
| import fr.free.nrw.commons.BuildConfig; | ||||
| import fr.free.nrw.commons.OkHttpConnectionFactory; | ||||
| import fr.free.nrw.commons.actions.PageEditClient; | ||||
| import fr.free.nrw.commons.actions.PageEditInterface; | ||||
| import fr.free.nrw.commons.actions.ThanksInterface; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; | ||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface; | ||||
| import fr.free.nrw.commons.auth.csrf.LogoutClient; | ||||
| import fr.free.nrw.commons.auth.login.LoginClient; | ||||
| import fr.free.nrw.commons.auth.login.LoginInterface; | ||||
| import fr.free.nrw.commons.category.CategoryInterface; | ||||
| import fr.free.nrw.commons.explore.depictions.DepictsClient; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
|  | @ -18,11 +26,15 @@ import fr.free.nrw.commons.media.PageMediaInterface; | |||
| import fr.free.nrw.commons.media.WikidataMediaInterface; | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | ||||
| import fr.free.nrw.commons.mwapi.UserInterface; | ||||
| import fr.free.nrw.commons.notification.NotificationInterface; | ||||
| import fr.free.nrw.commons.review.ReviewInterface; | ||||
| import fr.free.nrw.commons.upload.UploadInterface; | ||||
| import fr.free.nrw.commons.upload.WikiBaseInterface; | ||||
| import fr.free.nrw.commons.upload.depicts.DepictsInterface; | ||||
| import fr.free.nrw.commons.wikidata.CommonsServiceFactory; | ||||
| import fr.free.nrw.commons.wikidata.WikidataInterface; | ||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; | ||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage; | ||||
| import java.io.File; | ||||
| import java.util.Locale; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | @ -33,31 +45,25 @@ import okhttp3.HttpUrl; | |||
| import okhttp3.OkHttpClient; | ||||
| import okhttp3.logging.HttpLoggingInterceptor; | ||||
| import okhttp3.logging.HttpLoggingInterceptor.Level; | ||||
| import org.wikipedia.csrf.CsrfTokenClient; | ||||
| import org.wikipedia.dataclient.Service; | ||||
| import org.wikipedia.dataclient.ServiceFactory; | ||||
| import org.wikipedia.dataclient.WikiSite; | ||||
| import org.wikipedia.json.GsonUtil; | ||||
| import org.wikipedia.login.LoginClient; | ||||
| import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||
| import fr.free.nrw.commons.wikidata.GsonUtil; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| @Module | ||||
| @SuppressWarnings({"WeakerAccess", "unused"}) | ||||
| public class NetworkingModule { | ||||
|     private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql"; | ||||
|     private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/urbanecmbot/commonsmisc"; | ||||
| 
 | ||||
|     private static final String TEST_TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"; | ||||
|     private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"; | ||||
| 
 | ||||
|     public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; | ||||
| 
 | ||||
|     public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite"; | ||||
|     private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"; | ||||
|     private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite"; | ||||
| 
 | ||||
|     public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite"; | ||||
| 
 | ||||
|     public static final String NAMED_COMMONS_CSRF = "commons-csrf"; | ||||
|     public static final String NAMED_WIKI_CSRF = "wiki-csrf"; | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|  | @ -73,6 +79,12 @@ public class NetworkingModule { | |||
|             .build(); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) { | ||||
|         return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public HttpLoggingInterceptor provideHttpLoggingInterceptor() { | ||||
|  | @ -88,29 +100,86 @@ public class NetworkingModule { | |||
|     public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, | ||||
|                                                           DepictsClient depictsClient, | ||||
|                                                           @Named("tools_forge") HttpUrl toolsForgeUrl, | ||||
|                                                           @Named("test_tools_forge") HttpUrl testToolsForgeUrl, | ||||
|                                                           @Named("default_preferences") JsonKvStore defaultKvStore, | ||||
|                                                           Gson gson) { | ||||
|         return new OkHttpJsonApiClient(okHttpClient, | ||||
|                 depictsClient, | ||||
|                 toolsForgeUrl, | ||||
|                 testToolsForgeUrl, | ||||
|                 WIKIDATA_SPARQL_QUERY_URL, | ||||
|                 BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, | ||||
|             gson); | ||||
|     } | ||||
| 
 | ||||
|     @Named(NAMED_COMMONS_CSRF) | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return new CsrfTokenClient(commonsWikiSite, commonsWikiSite); | ||||
|     public CommonsCookieStorage provideCookieStorage( | ||||
|         @Named("default_preferences") JsonKvStore preferences) { | ||||
|         CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences); | ||||
|         cookieStorage.load(); | ||||
|         return cookieStorage; | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public LoginClient provideLoginClient() { | ||||
|         return new LoginClient(); | ||||
|     public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) { | ||||
|         return new CommonsCookieJar(storage); | ||||
|     } | ||||
| 
 | ||||
|     @Named(NAMED_COMMONS_CSRF) | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager, | ||||
|         @Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { | ||||
|         return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provides a singleton instance of CsrfTokenClient for Wikidata. | ||||
|      * | ||||
|      * @param sessionManager The session manager to manage user sessions. | ||||
|      * @param tokenInterface The interface for obtaining CSRF tokens. | ||||
|      * @param loginClient    The client for handling login operations. | ||||
|      * @param logoutClient   The client for handling logout operations. | ||||
|      * @return A singleton instance of CsrfTokenClient. | ||||
|      */ | ||||
|     @Named(NAMED_WIKI_CSRF) | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager, | ||||
|         @Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { | ||||
|         return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provides a singleton instance of CsrfTokenInterface for Wikidata. | ||||
|      * | ||||
|      * @param serviceFactory The factory used to create service interfaces. | ||||
|      * @return A singleton instance of CsrfTokenInterface for Wikidata. | ||||
|      */ | ||||
|     @Named("wikidata-csrf-interface") | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Named("commons-csrf-interface") | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public LoginClient provideLoginClient(LoginInterface loginInterface) { | ||||
|         return new LoginClient(loginInterface); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|  | @ -129,21 +198,6 @@ public class NetworkingModule { | |||
|         return HttpUrl.parse(TOOLS_FORGE_URL); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Named("test_tools_forge") | ||||
|     @NonNull | ||||
|     @SuppressWarnings("ConstantConditions") | ||||
|     public HttpUrl provideTestToolsForgeUrl() { | ||||
|         return HttpUrl.parse(TEST_TOOLS_FORGE_URL); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     @Named(NAMED_COMMONS_WIKI_SITE) | ||||
|     public WikiSite provideCommonsWikiSite() { | ||||
|         return new WikiSite(BuildConfig.COMMONS_URL); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     @Named(NAMED_WIKI_DATA_WIKI_SITE) | ||||
|  | @ -164,54 +218,40 @@ public class NetworkingModule { | |||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     @Named("commons-service") | ||||
|     public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite); | ||||
|     public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     @Named("wikidata-service") | ||||
|     public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { | ||||
|         return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class); | ||||
|     public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); | ||||
|     public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { | ||||
|         return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public WikiBaseInterface provideWikiBaseInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, WikiBaseInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class); | ||||
|     public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Named("commons-page-edit-service") | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class); | ||||
|     public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Named("wikidata-page-edit-service") | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { | ||||
|         return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class); | ||||
|     public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Named("commons-page-edit") | ||||
|  | @ -222,10 +262,25 @@ public class NetworkingModule { | |||
|         return new PageEditClient(csrfTokenClient, pageEditInterface); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provides a singleton instance of PageEditClient for Wikidata. | ||||
|      * | ||||
|      * @param csrfTokenClient    The client used to manage CSRF tokens. | ||||
|      * @param pageEditInterface  The interface for page edit operations. | ||||
|      * @return A singleton instance of PageEditClient for Wikidata. | ||||
|      */ | ||||
|     @Named("wikidata-page-edit") | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class); | ||||
|     public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient, | ||||
|         @Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) { | ||||
|         return new PageEditClient(csrfTokenClient, pageEditInterface); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -236,36 +291,44 @@ public class NetworkingModule { | |||
|      */ | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public WikidataMediaInterface provideWikidataMediaInterface( | ||||
|         @Named(NAMED_COMMONS_WIKI_SITE) final WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, | ||||
|             BetaConstants.COMMONS_URL, WikidataMediaInterface.class); | ||||
|     public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) { | ||||
|         return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class); | ||||
|     public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public CategoryInterface provideCategoryInterface( | ||||
|         @Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory | ||||
|                .get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class); | ||||
|     public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { | ||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UserInterface.class); | ||||
|     public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public WikidataInterface provideWikidataInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { | ||||
|         return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, WikidataInterface.class); | ||||
|     public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -274,8 +337,8 @@ public class NetworkingModule { | |||
|      */ | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite) { | ||||
|         return ServiceFactory.get(wikiSite, wikiSite.url(), PageMediaInterface.class); | ||||
|     public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) { | ||||
|         return serviceFactory.create(wikiSite.url(), PageMediaInterface.class); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|  |  | |||
							
								
								
									
										301
									
								
								app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,301 @@ | |||
| package fr.free.nrw.commons.edit | ||||
| 
 | ||||
| import android.animation.Animator | ||||
| import android.animation.Animator.AnimatorListener | ||||
| import android.animation.ValueAnimator | ||||
| import android.content.Intent | ||||
| import android.graphics.BitmapFactory | ||||
| import android.graphics.Matrix | ||||
| import android.media.ExifInterface | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| import android.view.animation.AccelerateDecelerateInterpolator | ||||
| import android.widget.ImageView | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.graphics.rotationMatrix | ||||
| import androidx.core.graphics.scaleMatrix | ||||
| import androidx.core.net.toUri | ||||
| import androidx.lifecycle.ViewModelProvider | ||||
| import fr.free.nrw.commons.databinding.ActivityEditBinding | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| 
 | ||||
| /** | ||||
|  * An activity class for editing and rotating images using LLJTran with EXIF attribute preservation. | ||||
|  * | ||||
|  * This activity allows loads an image, allows users to rotate it by 90-degree increments, and | ||||
|  * save the edited image while preserving its EXIF attributes. The class includes methods | ||||
|  * for initializing the UI, animating image rotations, copying EXIF data, and handling | ||||
|  * the image-saving process. | ||||
|  */ | ||||
| class EditActivity : AppCompatActivity() { | ||||
|     private var imageUri = "" | ||||
|     private lateinit var vm: EditViewModel | ||||
|     private val sourceExifAttributeList = mutableListOf<Pair<String, String?>>() | ||||
|     private lateinit var binding: ActivityEditBinding | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = ActivityEditBinding.inflate(layoutInflater) | ||||
|         setContentView(binding.root) | ||||
|         supportActionBar?.title = "" | ||||
|         val intent = intent | ||||
|         imageUri = intent.getStringExtra("image") ?: "" | ||||
|         vm = ViewModelProvider(this).get(EditViewModel::class.java) | ||||
|         val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) } | ||||
|         val exifTags = arrayOf( | ||||
|             ExifInterface.TAG_APERTURE, | ||||
|             ExifInterface.TAG_DATETIME, | ||||
|             ExifInterface.TAG_EXPOSURE_TIME, | ||||
|             ExifInterface.TAG_FLASH, | ||||
|             ExifInterface.TAG_FOCAL_LENGTH, | ||||
|             ExifInterface.TAG_GPS_ALTITUDE, | ||||
|             ExifInterface.TAG_GPS_ALTITUDE_REF, | ||||
|             ExifInterface.TAG_GPS_DATESTAMP, | ||||
|             ExifInterface.TAG_GPS_LATITUDE, | ||||
|             ExifInterface.TAG_GPS_LATITUDE_REF, | ||||
|             ExifInterface.TAG_GPS_LONGITUDE, | ||||
|             ExifInterface.TAG_GPS_LONGITUDE_REF, | ||||
|             ExifInterface.TAG_GPS_PROCESSING_METHOD, | ||||
|             ExifInterface.TAG_GPS_TIMESTAMP, | ||||
|             ExifInterface.TAG_IMAGE_LENGTH, | ||||
|             ExifInterface.TAG_IMAGE_WIDTH, | ||||
|             ExifInterface.TAG_ISO, | ||||
|             ExifInterface.TAG_MAKE, | ||||
|             ExifInterface.TAG_MODEL, | ||||
|             ExifInterface.TAG_ORIENTATION, | ||||
|             ExifInterface.TAG_WHITE_BALANCE, | ||||
|             ExifInterface.WHITEBALANCE_AUTO, | ||||
|             ExifInterface.WHITEBALANCE_MANUAL | ||||
|         ) | ||||
|         for (tag in exifTags) { | ||||
|             val attribute = sourceExif?.getAttribute(tag.toString()) | ||||
|             sourceExifAttributeList.add(Pair(tag.toString(), attribute)) | ||||
|         } | ||||
| 
 | ||||
|         init() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes the ImageView and associated UI elements. | ||||
|      * | ||||
|      * This function sets up the ImageView for displaying an image, adjusts its view bounds, | ||||
|      * and scales the initial image to fit within the ImageView. It also sets click listeners | ||||
|      * for the "Rotate" and "Save" buttons. | ||||
|      */ | ||||
|     private fun init() { | ||||
|         binding.iv.adjustViewBounds = true | ||||
|         binding.iv.scaleType = ImageView.ScaleType.MATRIX | ||||
|         binding.iv.post(Runnable { | ||||
|             val options = BitmapFactory.Options() | ||||
|             options.inJustDecodeBounds = true | ||||
|             BitmapFactory.decodeFile(imageUri, options) | ||||
| 
 | ||||
|             val bitmapWidth = options.outWidth | ||||
|             val bitmapHeight = options.outHeight | ||||
| 
 | ||||
|             // Check if the bitmap dimensions exceed a certain threshold | ||||
|             val maxBitmapSize = 2000 // Set your maximum size here | ||||
|             if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) { | ||||
|                 val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize) | ||||
|                 options.inSampleSize = scaleFactor | ||||
|                 options.inJustDecodeBounds = false | ||||
|                 val scaledBitmap = BitmapFactory.decodeFile(imageUri, options) | ||||
|                 binding.iv.setImageBitmap(scaledBitmap) | ||||
|                 // Update the ImageView with the scaled bitmap | ||||
|                 val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat() | ||||
|                 binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt() | ||||
|                 binding.iv.imageMatrix = scaleMatrix(scale, scale) | ||||
|             } else { | ||||
| 
 | ||||
|                 options.inJustDecodeBounds = false | ||||
|                 val bitmap = BitmapFactory.decodeFile(imageUri, options) | ||||
|                 binding.iv.setImageBitmap(bitmap) | ||||
| 
 | ||||
|                 val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat() | ||||
|                 binding.iv.layoutParams.height = (scale * bitmapHeight).toInt() | ||||
|                 binding.iv.imageMatrix = scaleMatrix(scale, scale) | ||||
|             } | ||||
|         }) | ||||
|         binding.rotateBtn.setOnClickListener { | ||||
|             animateImageHeight() | ||||
|         } | ||||
|         binding.btnSave.setOnClickListener { | ||||
|             getRotatedImage() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     var imageRotation = 0 | ||||
| 
 | ||||
|     /** | ||||
|      * Animates the height, rotation, and scale of an ImageView to provide a smooth | ||||
|      * transition effect when rotating an image by 90 degrees. | ||||
|      * | ||||
|      * This function calculates the new height, rotation, and scale for the ImageView | ||||
|      * based on the current image rotation angle and animates the changes using a | ||||
|      * ValueAnimator. It also disables a rotate button during the animation to prevent | ||||
|      * further rotation actions. | ||||
|      */ | ||||
|     private fun animateImageHeight() { | ||||
|         val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat() | ||||
|         val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat() | ||||
|         val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat() | ||||
|         val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat() | ||||
|         val rotation = imageRotation % 360 | ||||
|         val newRotation = rotation + 90 | ||||
| 
 | ||||
|         val newViewHeight: Int | ||||
|         val imageScale: Float | ||||
|         val newImageScale: Float | ||||
| 
 | ||||
|         Timber.d("Rotation $rotation") | ||||
|         Timber.d("new Rotation $newRotation") | ||||
| 
 | ||||
| 
 | ||||
|         if (rotation == 0 || rotation == 180) { | ||||
|             imageScale = viewWidth / drawableWidth | ||||
|             newImageScale = viewWidth / drawableHeight | ||||
|             newViewHeight = (drawableWidth * newImageScale).toInt() | ||||
|         } else if (rotation == 90 || rotation == 270) { | ||||
|             imageScale = viewWidth / drawableHeight | ||||
|             newImageScale = viewWidth / drawableWidth | ||||
|             newViewHeight = (drawableHeight * newImageScale).toInt() | ||||
|         } else { | ||||
|             throw UnsupportedOperationException("rotation can 0, 90, 180 or 270. \${rotation} is unsupported") | ||||
|         } | ||||
| 
 | ||||
|         val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L) | ||||
| 
 | ||||
|         animator.interpolator = AccelerateDecelerateInterpolator() | ||||
| 
 | ||||
|         animator.addListener(object : AnimatorListener { | ||||
|             override fun onAnimationStart(animation: Animator) { | ||||
|                 binding.rotateBtn.setEnabled(false) | ||||
|             } | ||||
| 
 | ||||
|             override fun onAnimationEnd(animation: Animator) { | ||||
|                 imageRotation = newRotation % 360 | ||||
|                 binding.rotateBtn.setEnabled(true) | ||||
|             } | ||||
| 
 | ||||
|             override fun onAnimationCancel(animation: Animator) { | ||||
|             } | ||||
| 
 | ||||
|             override fun onAnimationRepeat(animation: Animator) { | ||||
|             } | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|         animator.addUpdateListener { animation -> | ||||
|             val animVal = animation.animatedValue as Float | ||||
|             val complementaryAnimVal = 1 - animVal | ||||
|             val animatedHeight = | ||||
|                 (complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt() | ||||
|             val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale | ||||
|             val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation | ||||
|             binding.iv.getLayoutParams().height = animatedHeight | ||||
|             val matrix: Matrix = rotationMatrix( | ||||
|                 animatedRotation, | ||||
|                 drawableWidth / 2, | ||||
|                 drawableHeight / 2 | ||||
|             ) | ||||
|             matrix.postScale( | ||||
|                 animatedScale, | ||||
|                 animatedScale, | ||||
|                 drawableWidth / 2, | ||||
|                 drawableHeight / 2 | ||||
|             ) | ||||
|             matrix.postTranslate( | ||||
|                 -(drawableWidth - binding.iv.getMeasuredWidth()) / 2, | ||||
|                 -(drawableHeight - binding.iv.getMeasuredHeight()) / 2 | ||||
|             ) | ||||
|             binding.iv.setImageMatrix(matrix) | ||||
|             binding.iv.requestLayout() | ||||
|         } | ||||
| 
 | ||||
|         animator.start() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Rotates and edits the current image, copies EXIF data, and returns the edited image path. | ||||
|      * | ||||
|      * This function retrieves the path of the current image specified by `imageUri`, | ||||
|      * rotates it based on the `imageRotation` angle using the `rotateImage` method | ||||
|      * from the `vm`, and updates the EXIF attributes of the | ||||
|      * rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data | ||||
|      * using the `copyExifData` method, creates an Intent to return the edited image's file path | ||||
|      * as a result, and finishes the current activity. | ||||
|      */ | ||||
|     fun getRotatedImage() { | ||||
| 
 | ||||
|         val filePath = imageUri.toUri().path | ||||
|         val file = filePath?.let { File(it) } | ||||
| 
 | ||||
| 
 | ||||
|         val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) } | ||||
|         if (rotatedImage == null) { | ||||
|             Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show() | ||||
|         } | ||||
|         val editedImageExif: ExifInterface? | ||||
|         if (rotatedImage?.path != null) { | ||||
|             editedImageExif = ExifInterface(rotatedImage.path) | ||||
|             copyExifData(editedImageExif) | ||||
|         } | ||||
|         val resultIntent = Intent() | ||||
|         resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error"); | ||||
|         setResult(RESULT_OK, resultIntent); | ||||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object. | ||||
|      * | ||||
|      * This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes | ||||
|      * on the provided `editedImageExif` object. | ||||
|      * | ||||
|      * @param editedImageExif The ExifInterface object for the edited image. | ||||
|      */ | ||||
|     private fun copyExifData(editedImageExif: ExifInterface?) { | ||||
| 
 | ||||
|         for (attr in sourceExifAttributeList) { | ||||
|             Log.d("Tag is  ${attr.first}", "Value is ${attr.second}") | ||||
|             editedImageExif!!.setAttribute(attr.first, attr.second) | ||||
|             Log.d("Tag is ${attr.first}", "Value is ${attr.second}") | ||||
|         } | ||||
| 
 | ||||
|         editedImageExif?.saveAttributes() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculates the scale factor to be used for scaling down a bitmap based on its original | ||||
|      *  dimensions and the maximum allowed size. | ||||
|      * @param originalWidth  The original width of the bitmap. | ||||
|      * @param originalHeight The original height of the bitmap. | ||||
|      * @param maxSize        The maximum allowed size for either width or height. | ||||
|      * @return The scale factor to be used for scaling down the bitmap. | ||||
|      *         If the bitmap is smaller than or equal to the maximum size in both dimensions, | ||||
|      *         the scale factor is 1. | ||||
|      *         If the bitmap is larger than the maximum size in either dimension, | ||||
|      *         the scale factor is calculated as the largest power of 2 that is less than or equal | ||||
|      *         to the ratio of the original dimension to the maximum size. | ||||
|      *         The scale factor ensures that the scaled bitmap will fit within the maximum size | ||||
|      *         while maintaining aspect ratio. | ||||
|      */ | ||||
|     private fun calculateScaleFactor(originalWidth: Int, originalHeight: Int, maxSize: Int): Int { | ||||
|         var scaleFactor = 1 | ||||
| 
 | ||||
|         if (originalWidth > maxSize || originalHeight > maxSize) { | ||||
|             // Calculate the largest power of 2 that is less than or equal to the desired width and height | ||||
|             val widthRatio = Math.ceil((originalWidth.toDouble() / maxSize.toDouble())).toInt() | ||||
|             val heightRatio = Math.ceil((originalHeight.toDouble() / maxSize.toDouble())).toInt() | ||||
| 
 | ||||
|             scaleFactor = if (widthRatio > heightRatio) widthRatio else heightRatio | ||||
|         } | ||||
| 
 | ||||
|         return scaleFactor | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										27
									
								
								app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| package fr.free.nrw.commons.edit | ||||
| 
 | ||||
| import androidx.lifecycle.ViewModel | ||||
| import java.io.File | ||||
| 
 | ||||
| /** | ||||
|  * ViewModel for image editing operations. | ||||
|  * | ||||
|  * This ViewModel class is responsible for managing image editing operations, such as | ||||
|  * rotating images. It utilizes a TransformImage implementation to perform image transformations. | ||||
|  */ | ||||
| class EditViewModel() : ViewModel() { | ||||
| 
 | ||||
|     // Ideally should be injected using DI | ||||
|     private val transformImage: TransformImage = TransformImageImpl() | ||||
| 
 | ||||
|     /** | ||||
|      * Rotates the specified image file by the given degree. | ||||
|      * | ||||
|      * @param degree The degree by which to rotate the image. | ||||
|      * @param imageFile The File representing the image to be rotated. | ||||
|      * @return The rotated image File, or null if the rotation operation fails. | ||||
|      */ | ||||
|     fun rotateImage(degree: Int, imageFile: File): File? { | ||||
|         return transformImage.rotateImage(imageFile, degree) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| package fr.free.nrw.commons.edit | ||||
| 
 | ||||
| import java.io.File | ||||
| 
 | ||||
| /** | ||||
|  * Interface for image transformation operations. | ||||
|  * | ||||
|  * This interface defines a contract for image transformation operations, allowing | ||||
|  * implementations to provide specific functionality for tasks like rotating images. | ||||
|  */ | ||||
| interface TransformImage { | ||||
| 
 | ||||
|     /** | ||||
|      * Rotates the specified image file by the given degree. | ||||
|      * | ||||
|      * @param imageFile The File representing the image to be rotated. | ||||
|      * @param degree The degree by which to rotate the image. | ||||
|      * @return The rotated image File, or null if the rotation operation fails. | ||||
|      */ | ||||
|     fun rotateImage(imageFile: File, degree : Int ):File? | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| package fr.free.nrw.commons.edit | ||||
| 
 | ||||
| import android.mediautil.image.jpeg.LLJTran | ||||
| import android.mediautil.image.jpeg.LLJTranException | ||||
| import android.os.Environment | ||||
| import timber.log.Timber | ||||
| import java.io.BufferedOutputStream | ||||
| import java.io.File | ||||
| import java.io.FileOutputStream | ||||
| 
 | ||||
| /** | ||||
|  * Implementation of the TransformImage interface for image rotation operations. | ||||
|  * | ||||
|  * This class provides an implementation for the TransformImage interface, right now it exposes a | ||||
|  * function for rotating images by a specified degree using the LLJTran library. Right now it reads | ||||
|  * the input image file, performs the rotation, and saves the rotated image to a new file. | ||||
|  */ | ||||
| class TransformImageImpl() : TransformImage { | ||||
| 
 | ||||
|     /** | ||||
|      * Rotates the specified image file by the given degree. | ||||
|      * | ||||
|      * @param imageFile The File representing the image to be rotated. | ||||
|      * @param degree The degree by which to rotate the image. | ||||
|      * @return The rotated image File, or null if the rotation operation fails. | ||||
|      */ | ||||
|     override fun rotateImage(imageFile: File, degree : Int): File? { | ||||
| 
 | ||||
|         Timber.tag("Trying to rotate image").d("Starting") | ||||
| 
 | ||||
|         val path = Environment.getExternalStoragePublicDirectory( | ||||
|             Environment.DIRECTORY_DOWNLOADS | ||||
|         ) | ||||
| 
 | ||||
|         val imagePath = System.currentTimeMillis() | ||||
|         val file: File = File(path, "$imagePath.jpg") | ||||
| 
 | ||||
|         val output = file | ||||
| 
 | ||||
|         val rotated = try { | ||||
|             val lljTran = LLJTran(imageFile) | ||||
|             lljTran.read( | ||||
|                 LLJTran.READ_ALL, | ||||
|                 false, | ||||
|             ) // This could throw an LLJTranException. I am not catching it for now... Let's see. | ||||
|             lljTran.transform( | ||||
|                 when(degree){ | ||||
|                          90 -> LLJTran.ROT_90 | ||||
|                          180 -> LLJTran.ROT_180 | ||||
|                          270 -> LLJTran.ROT_270 | ||||
|                     else -> { | ||||
|                       LLJTran.ROT_90 | ||||
|                     } | ||||
|                 }, | ||||
|                 LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION | ||||
|             ) | ||||
|             BufferedOutputStream(FileOutputStream(output)).use { writer -> | ||||
|                 lljTran.save(writer, LLJTran.OPT_WRITE_ALL ) | ||||
|             } | ||||
|             lljTran.freeMemory() | ||||
|             true | ||||
|         } catch (e: LLJTranException) { | ||||
|             Timber.tag("Error").d(e) | ||||
|             return null | ||||
|             false | ||||
|         } | ||||
| 
 | ||||
|         if (rotated) { | ||||
|             Timber.tag("Done rotating image").d("Done") | ||||
|             Timber.tag("Add").d(output.absolutePath) | ||||
|         } | ||||
|         return output | ||||
|     } | ||||
| } | ||||
|  | @ -11,12 +11,11 @@ import androidx.annotation.NonNull; | |||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.viewpager.widget.ViewPager.OnPageChangeListener; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import com.google.android.material.tabs.TabLayout; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.ViewPagerAdapter; | ||||
| import fr.free.nrw.commons.contributions.MainActivity; | ||||
| import fr.free.nrw.commons.databinding.FragmentExploreBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
|  | @ -33,10 +32,8 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|     private static final String EXPLORE_MAP = "Map"; | ||||
|     private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment"; | ||||
| 
 | ||||
|     @BindView(R.id.tab_layout) | ||||
|     TabLayout tabLayout; | ||||
|     @BindView(R.id.viewPager) | ||||
|     ParentViewPager viewPager; | ||||
| 
 | ||||
|     public FragmentExploreBinding binding; | ||||
|     ViewPagerAdapter viewPagerAdapter; | ||||
|     private ExploreListRootFragment featuredRootFragment; | ||||
|     private ExploreListRootFragment mobileRootFragment; | ||||
|  | @ -46,7 +43,10 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|     public JsonKvStore applicationKvStore; | ||||
| 
 | ||||
|     public void setScroll(boolean canScroll){ | ||||
|         viewPager.setCanScroll(canScroll); | ||||
|         if (binding != null) | ||||
|         { | ||||
|             binding.viewPager.setCanScroll(canScroll); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|  | @ -56,22 +56,17 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|         return fragment; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         View view = inflater.inflate(R.layout.fragment_explore, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         binding = FragmentExploreBinding.inflate(inflater, container, false); | ||||
| 
 | ||||
|         viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); | ||||
|         viewPager.setAdapter(viewPagerAdapter); | ||||
|         viewPager.setId(R.id.viewPager); | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         viewPager.addOnPageChangeListener(new OnPageChangeListener() { | ||||
|         binding.viewPager.setAdapter(viewPagerAdapter); | ||||
|         binding.viewPager.setId(R.id.viewPager); | ||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||
|         binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { | ||||
|             @Override | ||||
|             public void onPageScrolled(int position, float positionOffset, | ||||
|                 int positionOffsetPixels) { | ||||
|  | @ -81,9 +76,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|             @Override | ||||
|             public void onPageSelected(int position) { | ||||
|                 if (position == 2) { | ||||
|                     viewPager.setCanScroll(false); | ||||
|                     binding.viewPager.setCanScroll(false); | ||||
|                 } else { | ||||
|                     viewPager.setCanScroll(true); | ||||
|                     binding.viewPager.setCanScroll(true); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -94,7 +89,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|         }); | ||||
|         setTabs(); | ||||
|         setHasOptionsMenu(true); | ||||
|         return view; | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -133,13 +128,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|     } | ||||
| 
 | ||||
|     public boolean onBackPressed() { | ||||
|         if (tabLayout.getSelectedTabPosition() == 0) { | ||||
|         if (binding.tabLayout.getSelectedTabPosition() == 0) { | ||||
|             if (featuredRootFragment.backPressed()) { | ||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||
|                     .setDisplayHomeAsUpEnabled(false); | ||||
|                 return true; | ||||
|             } | ||||
|         } else if (tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment | ||||
|         } else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment | ||||
|             if (mobileRootFragment.backPressed()) { | ||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||
|                     .setDisplayHomeAsUpEnabled(false); | ||||
|  | @ -180,6 +175,12 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,12 +9,11 @@ import android.widget.FrameLayout; | |||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||
| 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.explore.categories.media.CategoriesMediaFragment; | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||
|  | @ -26,8 +25,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | |||
|     private MediaDetailPagerFragment mediaDetails; | ||||
|     private CategoriesMediaFragment listFragment; | ||||
| 
 | ||||
|     @BindView(R.id.explore_container) | ||||
|     FrameLayout container; | ||||
|     private FragmentFeaturedRootBinding binding; | ||||
| 
 | ||||
|     public ExploreListRootFragment() { | ||||
|         //empty constructor necessary otherwise crashes on recreate | ||||
|  | @ -47,9 +45,9 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | |||
|         @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         return view; | ||||
| 
 | ||||
|         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -109,9 +107,13 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | |||
| 
 | ||||
|     @Override | ||||
|     public void onMediaClicked(int position) { | ||||
|         container.setVisibility(View.VISIBLE); | ||||
|         ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|         if (binding!=null) { | ||||
|             binding.exploreContainer.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|         if (((ExploreFragment) getParentFragment()).binding!=null) { | ||||
|             ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); | ||||
|         } | ||||
|         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||
|         ((ExploreFragment) getParentFragment()).setScroll(false); | ||||
|         setFragment(mediaDetails, listFragment); | ||||
|         mediaDetails.showImage(position); | ||||
|  | @ -185,16 +187,29 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | |||
|      */ | ||||
|     public boolean backPressed() { | ||||
|         if (null != mediaDetails && mediaDetails.isVisible()) { | ||||
|             ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE); | ||||
|             if (((ExploreFragment) getParentFragment()).binding != null) { | ||||
|                 ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             } | ||||
|             removeFragment(mediaDetails); | ||||
|             ((ExploreFragment) getParentFragment()).setScroll(true); | ||||
|             setFragment(listFragment, mediaDetails); | ||||
|             ((MainActivity) getActivity()).showTabs(); | ||||
|             return true; | ||||
|         } else { | ||||
|             ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||
|             if (((MainActivity) getActivity()) != null) { | ||||
|                 ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||
|             } | ||||
|         } | ||||
|         if (((MainActivity) getActivity()) != null) { | ||||
|             ((MainActivity) getActivity()).showTabs(); | ||||
|         } | ||||
|         ((MainActivity) getActivity()).showTabs(); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
| 
 | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -9,12 +9,11 @@ import android.widget.FrameLayout; | |||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||
| 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.explore.map.ExploreMapFragment; | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||
|  | @ -26,8 +25,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | |||
|     private MediaDetailPagerFragment mediaDetails; | ||||
|     private ExploreMapFragment mapFragment; | ||||
| 
 | ||||
|     @BindView(R.id.explore_container) | ||||
|     FrameLayout container; | ||||
|     private FragmentFeaturedRootBinding binding; | ||||
| 
 | ||||
|     public ExploreMapRootFragment() { | ||||
|         //empty constructor necessary otherwise crashes on recreate | ||||
|  | @ -54,9 +52,10 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | |||
|         @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|         return view; | ||||
| 
 | ||||
|         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||
| 
 | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -116,9 +115,9 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | |||
| 
 | ||||
|     @Override | ||||
|     public void onMediaClicked(int position) { | ||||
|         container.setVisibility(View.VISIBLE); | ||||
|         ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|         binding.exploreContainer.setVisibility(View.VISIBLE); | ||||
|         ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); | ||||
|         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||
|         ((ExploreFragment) getParentFragment()).setScroll(false); | ||||
|         setFragment(mediaDetails, mapFragment); | ||||
|         mediaDetails.showImage(position); | ||||
|  | @ -192,7 +191,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | |||
|      */ | ||||
|     public boolean backPressed() { | ||||
|         if (null != mediaDetails && mediaDetails.isVisible()) { | ||||
|             ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE); | ||||
|             ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             removeFragment(mediaDetails); | ||||
|             ((ExploreFragment) getParentFragment()).setScroll(true); | ||||
|             setFragment(mapFragment, mediaDetails); | ||||
|  | @ -213,4 +212,11 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | |||
|         ((MainActivity) getActivity()).showTabs(); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
| 
 | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,23 +3,17 @@ package fr.free.nrw.commons.explore; | |||
| import android.os.Bundle; | ||||
| import android.text.TextUtils; | ||||
| import android.view.View; | ||||
| import android.widget.FrameLayout; | ||||
| import android.widget.SearchView; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.appcompat.widget.Toolbar; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.fragment.app.FragmentTransaction; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import com.google.android.material.tabs.TabLayout; | ||||
| import com.jakewharton.rxbinding2.view.RxView; | ||||
| import com.jakewharton.rxbinding2.widget.RxSearchView; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.ViewPagerAdapter; | ||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||
| import fr.free.nrw.commons.databinding.ActivitySearchBinding; | ||||
| import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; | ||||
| import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; | ||||
| import fr.free.nrw.commons.explore.media.SearchMediaFragment; | ||||
|  | @ -45,13 +39,6 @@ import timber.log.Timber; | |||
| public class SearchActivity extends BaseActivity | ||||
|         implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { | ||||
| 
 | ||||
|     @BindView(R.id.toolbar_search) Toolbar toolbar; | ||||
|     @BindView(R.id.searchHistoryContainer) FrameLayout searchHistoryContainer; | ||||
|     @BindView(R.id.mediaContainer) FrameLayout mediaContainer; | ||||
|     @BindView(R.id.searchBox) SearchView searchView; | ||||
|     @BindView(R.id.tab_layout) TabLayout tabLayout; | ||||
|     @BindView(R.id.viewPager) ViewPager viewPager; | ||||
| 
 | ||||
|     @Inject | ||||
|     RecentSearchesDao recentSearchesDao; | ||||
| 
 | ||||
|  | @ -63,25 +50,28 @@ public class SearchActivity extends BaseActivity | |||
|     private MediaDetailPagerFragment mediaDetails; | ||||
|     ViewPagerAdapter viewPagerAdapter; | ||||
| 
 | ||||
|     private ActivitySearchBinding binding; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_search); | ||||
|         ButterKnife.bind(this); | ||||
|         binding = ActivitySearchBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
| 
 | ||||
|         setTitle(getString(R.string.title_activity_search)); | ||||
|         setSupportActionBar(toolbar); | ||||
|         setSupportActionBar(binding.toolbarSearch); | ||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|         toolbar.setNavigationOnClickListener(v->onBackPressed()); | ||||
|         binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed()); | ||||
|         supportFragmentManager = getSupportFragmentManager(); | ||||
|         setSearchHistoryFragment(); | ||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||
|         viewPager.setAdapter(viewPagerAdapter); | ||||
|         viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         binding.viewPager.setAdapter(viewPagerAdapter); | ||||
|         binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive | ||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||
|         setTabs(); | ||||
|         searchView.setQueryHint(getString(R.string.search_commons)); | ||||
|         searchView.onActionViewExpanded(); | ||||
|         searchView.clearFocus(); | ||||
|         binding.searchBox.setQueryHint(getString(R.string.search_commons)); | ||||
|         binding.searchBox.onActionViewExpanded(); | ||||
|         binding.searchBox.clearFocus(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -113,8 +103,8 @@ public class SearchActivity extends BaseActivity | |||
| 
 | ||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||
|         viewPagerAdapter.notifyDataSetChanged(); | ||||
|         compositeDisposable.add(RxSearchView.queryTextChanges(searchView) | ||||
|                 .takeUntil(RxView.detaches(searchView)) | ||||
|         compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox) | ||||
|                 .takeUntil(RxView.detaches(binding.searchBox)) | ||||
|                 .debounce(500, TimeUnit.MILLISECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(this::handleSearch, Timber::e | ||||
|  | @ -124,9 +114,9 @@ public class SearchActivity extends BaseActivity | |||
|     private void handleSearch(final CharSequence query) { | ||||
|         if (!TextUtils.isEmpty(query)) { | ||||
|             saveRecentSearch(query.toString()); | ||||
|             viewPager.setVisibility(View.VISIBLE); | ||||
|             tabLayout.setVisibility(View.VISIBLE); | ||||
|             searchHistoryContainer.setVisibility(View.GONE); | ||||
|             binding.viewPager.setVisibility(View.VISIBLE); | ||||
|             binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             binding.searchHistoryContainer.setVisibility(View.GONE); | ||||
| 
 | ||||
|             if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { | ||||
|                 searchDepictionsFragment.onQueryUpdated(query.toString()); | ||||
|  | @ -144,10 +134,10 @@ public class SearchActivity extends BaseActivity | |||
|         else { | ||||
|             //Open RecentSearchesFragment | ||||
|             recentSearchesFragment.updateRecentSearches(); | ||||
|             viewPager.setVisibility(View.GONE); | ||||
|             tabLayout.setVisibility(View.GONE); | ||||
|             binding.viewPager.setVisibility(View.GONE); | ||||
|             binding.tabLayout.setVisibility(View.GONE); | ||||
|             setSearchHistoryFragment(); | ||||
|             searchHistoryContainer.setVisibility(View.VISIBLE); | ||||
|             binding.searchHistoryContainer.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -215,13 +205,13 @@ public class SearchActivity extends BaseActivity | |||
|     @Override | ||||
|     public void onMediaClicked(int index) { | ||||
|         ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); | ||||
|         tabLayout.setVisibility(View.GONE); | ||||
|         viewPager.setVisibility(View.GONE); | ||||
|         mediaContainer.setVisibility(View.VISIBLE); | ||||
|         searchView.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open | ||||
|         binding.tabLayout.setVisibility(View.GONE); | ||||
|         binding.viewPager.setVisibility(View.GONE); | ||||
|         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||
|         binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open | ||||
|         if (mediaDetails == null || !mediaDetails.isVisible()) { | ||||
|             // set isFeaturedImage true for featured images, to include author field on media detail | ||||
|             mediaDetails = new MediaDetailPagerFragment(false, true); | ||||
|             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             supportFragmentManager | ||||
|                     .beginTransaction() | ||||
|                     .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) | ||||
|  | @ -269,12 +259,12 @@ public class SearchActivity extends BaseActivity | |||
|         } | ||||
|         if (getSupportFragmentManager().getBackStackEntryCount() == 1) { | ||||
|             // back to search so show search toolbar and hide navigation toolbar | ||||
|             searchView.setVisibility(View.VISIBLE);//set the searchview | ||||
|             tabLayout.setVisibility(View.VISIBLE); | ||||
|             viewPager.setVisibility(View.VISIBLE); | ||||
|             mediaContainer.setVisibility(View.GONE); | ||||
|             binding.searchBox.setVisibility(View.VISIBLE);//set the searchview | ||||
|             binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             binding.viewPager.setVisibility(View.VISIBLE); | ||||
|             binding.mediaContainer.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             toolbar.setVisibility(View.GONE); | ||||
|             binding.toolbarSearch.setVisibility(View.GONE); | ||||
|         } | ||||
|         super.onBackPressed(); | ||||
|     } | ||||
|  | @ -284,15 +274,16 @@ public class SearchActivity extends BaseActivity | |||
|      * @param query Recent Search Query | ||||
|      */ | ||||
|     public void updateText(String query) { | ||||
|         searchView.setQuery(query, true); | ||||
|         binding.searchBox.setQuery(query, true); | ||||
|         // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. | ||||
|         // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 | ||||
|         viewPager.requestFocus(); | ||||
|         binding.viewPager.requestFocus(); | ||||
|     } | ||||
| 
 | ||||
|     @Override protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         //Dispose the disposables when the activity is destroyed | ||||
|         compositeDisposable.dispose(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,11 +7,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsInterface | |||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||
| import fr.free.nrw.commons.upload.structure.depictions.get | ||||
| import fr.free.nrw.commons.wikidata.WikidataProperties | ||||
| import fr.free.nrw.commons.wikidata.model.DataValue | ||||
| import fr.free.nrw.commons.wikidata.model.DepictSearchItem | ||||
| import fr.free.nrw.commons.wikidata.model.Entities | ||||
| import fr.free.nrw.commons.wikidata.model.Statement_partial | ||||
| import io.reactivex.Single | ||||
| import org.wikipedia.wikidata.DataValue | ||||
| import org.wikipedia.wikidata.Entities | ||||
| import org.wikipedia.wikidata.Statement_partial | ||||
| import java.util.* | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
|  |  | |||
|  | @ -13,8 +13,6 @@ import androidx.appcompat.widget.Toolbar; | |||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import butterknife.BindView; | ||||
| import butterknife.ButterKnife; | ||||
| import com.google.android.material.snackbar.Snackbar; | ||||
| import com.google.android.material.tabs.TabLayout; | ||||
| import fr.free.nrw.commons.Media; | ||||
|  | @ -23,6 +21,7 @@ import fr.free.nrw.commons.Utils; | |||
| import fr.free.nrw.commons.ViewPagerAdapter; | ||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; | ||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||
| import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding; | ||||
| import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; | ||||
| import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; | ||||
| import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; | ||||
|  | @ -57,14 +56,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | |||
|     @Inject | ||||
|     DepictModel depictModel; | ||||
|     private String wikidataItemName; | ||||
|     @BindView(R.id.mediaContainer) | ||||
|     FrameLayout mediaContainer; | ||||
|     @BindView(R.id.tab_layout) | ||||
|     TabLayout tabLayout; | ||||
|     @BindView(R.id.viewPager) | ||||
|     ViewPager viewPager; | ||||
|     @BindView(R.id.toolbar) | ||||
|     Toolbar toolbar; | ||||
|     private ActivityWikidataItemDetailsBinding binding; | ||||
| 
 | ||||
|     ViewPagerAdapter viewPagerAdapter; | ||||
|     private DepictedItem wikidataItem; | ||||
|  | @ -72,19 +64,20 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | |||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_wikidata_item_details); | ||||
|         ButterKnife.bind(this); | ||||
| 
 | ||||
|         binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|         supportFragmentManager = getSupportFragmentManager(); | ||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||
|         viewPager.setAdapter(viewPagerAdapter); | ||||
|         viewPager.setOffscreenPageLimit(2); | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|         binding.viewPager.setAdapter(viewPagerAdapter); | ||||
|         binding.viewPager.setOffscreenPageLimit(2); | ||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||
| 
 | ||||
|         final DepictedItem depictedItem = getIntent().getParcelableExtra( | ||||
|             WikidataConstants.BOOKMARKS_ITEMS); | ||||
|         wikidataItem = depictedItem; | ||||
|         setSupportActionBar(toolbar); | ||||
|         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|         setTabs(); | ||||
|         setPageTitle(); | ||||
|  | @ -137,7 +130,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | |||
|         fragmentList.add(parentDepictionsFragment); | ||||
|         titleList.add(getResources().getString(R.string.title_for_parent_classes)); | ||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||
|         viewPager.setOffscreenPageLimit(2); | ||||
|         binding.viewPager.setOffscreenPageLimit(2); | ||||
|         viewPagerAdapter.notifyDataSetChanged(); | ||||
| 
 | ||||
|     } | ||||
|  | @ -148,12 +141,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | |||
|      */ | ||||
|     @Override | ||||
|     public void onMediaClicked(int position) { | ||||
|         tabLayout.setVisibility(View.GONE); | ||||
|         viewPager.setVisibility(View.GONE); | ||||
|         mediaContainer.setVisibility(View.VISIBLE); | ||||
|         binding.tabLayout.setVisibility(View.GONE); | ||||
|         binding.viewPager.setVisibility(View.GONE); | ||||
|         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { | ||||
|             // set isFeaturedImage true for featured images, to include author field on media detail | ||||
|             mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             FragmentManager supportFragmentManager = getSupportFragmentManager(); | ||||
|             supportFragmentManager | ||||
|                     .beginTransaction() | ||||
|  | @ -183,9 +176,9 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | |||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         if (supportFragmentManager.getBackStackEntryCount() == 1){ | ||||
|             tabLayout.setVisibility(View.VISIBLE); | ||||
|             viewPager.setVisibility(View.VISIBLE); | ||||
|             mediaContainer.setVisibility(View.GONE); | ||||
|             binding.tabLayout.setVisibility(View.VISIBLE); | ||||
|             binding.viewPager.setVisibility(View.VISIBLE); | ||||
|             binding.mediaContainer.setVisibility(View.GONE); | ||||
|         } | ||||
|         super.onBackPressed(); | ||||
|     } | ||||
|  |  | |||
|  | @ -20,11 +20,11 @@ public class ExploreMapCalls { | |||
|     /** | ||||
|      * Calls method to query Commons for uploads around a location | ||||
|      * | ||||
|      * @param curLatLng coordinates of search location | ||||
|      * @param currentLatLng coordinates of search location | ||||
|      * @return list of places obtained | ||||
|      */ | ||||
|     List<Media> callCommonsQuery(final LatLng curLatLng) { | ||||
|         String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude(); | ||||
|     List<Media> callCommonsQuery(final LatLng currentLatLng) { | ||||
|         String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude(); | ||||
|         return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,45 +1,34 @@ | |||
| package fr.free.nrw.commons.explore.map; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import com.mapbox.mapboxsdk.annotations.Marker; | ||||
| import com.mapbox.mapboxsdk.camera.CameraUpdate; | ||||
| import fr.free.nrw.commons.BaseMarker; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.nearby.NearbyBaseMarker; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class ExploreMapContract { | ||||
| 
 | ||||
|     interface View { | ||||
|         boolean isNetworkConnectionEstablished(); | ||||
|         void populatePlaces(LatLng curlatLng,LatLng searchLatLng); | ||||
|         void checkPermissionsAndPerformAction(); | ||||
|         void populatePlaces(LatLng curlatLng); | ||||
|         void askForLocationPermission(); | ||||
|         void recenterMap(LatLng curLatLng); | ||||
|         void showLocationOffDialog(); | ||||
|         void openLocationSettings(); | ||||
|         void hideBottomDetailsSheet(); | ||||
|         void displayBottomSheetWithInfo(Marker marker); | ||||
|         void addOnCameraMoveListener(); | ||||
|         LatLng getMapCenter(); | ||||
|         LatLng getMapFocus(); | ||||
|         LatLng getLastMapFocus(); | ||||
|         void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers); | ||||
|         void clearAllMarkers(); | ||||
|         void addSearchThisAreaButtonAction(); | ||||
|         void setSearchThisAreaButtonVisibility(boolean isVisible); | ||||
|         void setProgressBarVisibility(boolean isVisible); | ||||
|         boolean isDetailsBottomSheetVisible(); | ||||
|         boolean isSearchThisAreaButtonVisible(); | ||||
|         void addCurrentLocationMarker(LatLng curLatLng); | ||||
|         void updateMapToTrackPosition(LatLng curLatLng); | ||||
|         Context getContext(); | ||||
|         LatLng getCameraTarget(); | ||||
|         void centerMapToPlace(Place placeToCenter); | ||||
|         LatLng getLastLocation(); | ||||
|         com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation(); | ||||
|         boolean isCurrentLocationMarkerVisible(); | ||||
|         void setProjectorLatLngBounds(); | ||||
|         void disableFABRecenter(); | ||||
|         void enableFABRecenter(); | ||||
|         void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker); | ||||
|         void setMapBoundaries(CameraUpdate cameaUpdate); | ||||
|         void setFABRecenterAction(android.view.View.OnClickListener onClickListener); | ||||
|         boolean backButtonClicked(); | ||||
|     } | ||||
|  | @ -51,9 +40,6 @@ public class ExploreMapContract { | |||
|         void detachView(); | ||||
|         void setActionListeners(JsonKvStore applicationKvStore); | ||||
|         boolean backButtonClicked(); | ||||
|         void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng); | ||||
|         void markerUnselected(); | ||||
|         void markerSelected(Marker marker); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -14,13 +14,11 @@ import com.bumptech.glide.Glide; | |||
| import com.bumptech.glide.request.RequestOptions; | ||||
| import com.bumptech.glide.request.target.CustomTarget; | ||||
| import com.bumptech.glide.request.transition.Transition; | ||||
| import com.mapbox.mapboxsdk.annotations.IconFactory; | ||||
| import com.mapbox.mapboxsdk.annotations.Marker; | ||||
| import fr.free.nrw.commons.BaseMarker; | ||||
| import fr.free.nrw.commons.MapController; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.nearby.NearbyBaseMarker; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.utils.ImageUtils; | ||||
| import fr.free.nrw.commons.utils.LocationUtils; | ||||
|  | @ -33,6 +31,7 @@ import javax.inject.Inject; | |||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class ExploreMapController extends MapController { | ||||
| 
 | ||||
|     private final ExploreMapCalls exploreMapCalls; | ||||
|     public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used | ||||
|     public LatLng currentLocation; // current location of user | ||||
|  | @ -46,13 +45,18 @@ public class ExploreMapController extends MapController { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Takes location as parameter and returns ExplorePlaces info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates | ||||
|      * @param curLatLng is current geolocation | ||||
|      * @param searchLatLng is the location that we want to search around | ||||
|      * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around current location, false if another location | ||||
|      * @return explorePlacesInfo info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates | ||||
|      * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, | ||||
|      * explorePlaceList and boundaryCoordinates | ||||
|      * | ||||
|      * @param currentLatLng                     is current geolocation | ||||
|      * @param searchLatLng                  is the location that we want to search around | ||||
|      * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around | ||||
|      *                                      current location, false if another location | ||||
|      * @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and | ||||
|      * boundaryCoordinates | ||||
|      */ | ||||
|     public ExplorePlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrentLocation) { | ||||
|     public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng, | ||||
|         boolean checkingAroundCurrentLocation) { | ||||
| 
 | ||||
|         if (searchLatLng == null) { | ||||
|             Timber.d("Loading attractions explore map, but search is null"); | ||||
|  | @ -61,7 +65,7 @@ public class ExploreMapController extends MapController { | |||
| 
 | ||||
|         ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo(); | ||||
|         try { | ||||
|             explorePlacesInfo.curLatLng = curLatLng; | ||||
|             explorePlacesInfo.currentLatLng = currentLatLng; | ||||
|             latestSearchLocation = searchLatLng; | ||||
| 
 | ||||
|             List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng); | ||||
|  | @ -74,18 +78,23 @@ public class ExploreMapController extends MapController { | |||
|                 Timber.d("Sorting places by distance..."); | ||||
|                 final Map<Media, Double> distances = new HashMap<>(); | ||||
|                 for (Media media : mediaList) { | ||||
|                     distances.put(media, computeDistanceBetween(media.getCoordinates(), searchLatLng)); | ||||
|                     distances.put(media, | ||||
|                         computeDistanceBetween(media.getCoordinates(), searchLatLng)); | ||||
|                     // Find boundaries with basic find max approach | ||||
|                     if (media.getCoordinates().getLatitude() < boundaryCoordinates[0].getLatitude()) { | ||||
|                     if (media.getCoordinates().getLatitude() | ||||
|                         < boundaryCoordinates[0].getLatitude()) { | ||||
|                         boundaryCoordinates[0] = media.getCoordinates(); | ||||
|                     } | ||||
|                     if (media.getCoordinates().getLatitude() > boundaryCoordinates[1].getLatitude()) { | ||||
|                     if (media.getCoordinates().getLatitude() | ||||
|                         > boundaryCoordinates[1].getLatitude()) { | ||||
|                         boundaryCoordinates[1] = media.getCoordinates(); | ||||
|                     } | ||||
|                     if (media.getCoordinates().getLongitude() < boundaryCoordinates[2].getLongitude()) { | ||||
|                     if (media.getCoordinates().getLongitude() | ||||
|                         < boundaryCoordinates[2].getLongitude()) { | ||||
|                         boundaryCoordinates[2] = media.getCoordinates(); | ||||
|                     } | ||||
|                     if (media.getCoordinates().getLongitude() > boundaryCoordinates[3].getLongitude()) { | ||||
|                     if (media.getCoordinates().getLongitude() | ||||
|                         > boundaryCoordinates[3].getLongitude()) { | ||||
|                         boundaryCoordinates[3] = media.getCoordinates(); | ||||
|                     } | ||||
|                 } | ||||
|  | @ -96,7 +105,8 @@ public class ExploreMapController extends MapController { | |||
| 
 | ||||
|             // Sets latestSearchRadius to maximum distance among boundaries and search location | ||||
|             for (LatLng bound : boundaryCoordinates) { | ||||
|                 double distance = LocationUtils.commonsLatLngToMapBoxLatLng(bound).distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(latestSearchLocation)); | ||||
|                 double distance = LocationUtils.calculateDistance(bound.getLatitude(), | ||||
|                     bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude()); | ||||
|                 if (distance > latestSearchRadius) { | ||||
|                     latestSearchRadius = distance; | ||||
|                 } | ||||
|  | @ -105,7 +115,7 @@ public class ExploreMapController extends MapController { | |||
|             // Our radius searched around us, will be used to understand when user search their own location, we will follow them | ||||
|             if (checkingAroundCurrentLocation) { | ||||
|                 currentLocationSearchRadius = latestSearchRadius; | ||||
|                 currentLocation = curLatLng; | ||||
|                 currentLocation = currentLatLng; | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|  | @ -115,42 +125,42 @@ public class ExploreMapController extends MapController { | |||
| 
 | ||||
|     /** | ||||
|      * Loads attractions from location for map view, we need to return places in Place data type | ||||
|      * | ||||
|      * @return baseMarkerOptions list that holds nearby places with their icons | ||||
|      */ | ||||
|     public static List<NearbyBaseMarker> loadAttractionsFromLocationToBaseMarkerOptions( | ||||
|         LatLng curLatLng, | ||||
|     public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions( | ||||
|         LatLng currentLatLng, | ||||
|         final List<Place> placeList, | ||||
|         Context context, | ||||
|         NearbyBaseMarkerThumbCallback callback, | ||||
|         Marker selectedMarker, | ||||
|         boolean shouldTrackPosition, | ||||
|         ExplorePlacesInfo explorePlacesInfo) { | ||||
|         List<NearbyBaseMarker> baseMarkerOptions = new ArrayList<>(); | ||||
|         List<BaseMarker> baseMarkerList = new ArrayList<>(); | ||||
| 
 | ||||
|         if (placeList == null) { | ||||
|             return baseMarkerOptions; | ||||
|             return baseMarkerList; | ||||
|         } | ||||
| 
 | ||||
|         VectorDrawableCompat vectorDrawable = null; | ||||
|         try { | ||||
|             vectorDrawable = VectorDrawableCompat.create( | ||||
|                 context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme()); | ||||
|                 context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme()); | ||||
| 
 | ||||
|         } catch (Resources.NotFoundException e) { | ||||
|             // ignore when running tests. | ||||
|         } | ||||
|         if (vectorDrawable != null) { | ||||
|             for (Place explorePlace : placeList) { | ||||
|                 final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); | ||||
|                 String distance = formatDistanceBetween(curLatLng, explorePlace.location); | ||||
|                 final BaseMarker baseMarker = new BaseMarker(); | ||||
|                 String distance = formatDistanceBetween(currentLatLng, explorePlace.location); | ||||
|                 explorePlace.setDistance(distance); | ||||
| 
 | ||||
|                 nearbyBaseMarker.title(explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); | ||||
|                 nearbyBaseMarker.position( | ||||
|                     new com.mapbox.mapboxsdk.geometry.LatLng( | ||||
|                 baseMarker.setTitle( | ||||
|                     explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); | ||||
|                 baseMarker.setPosition( | ||||
|                     new fr.free.nrw.commons.location.LatLng( | ||||
|                         explorePlace.location.getLatitude(), | ||||
|                         explorePlace.location.getLongitude())); | ||||
|                 nearbyBaseMarker.place(explorePlace); | ||||
|                         explorePlace.location.getLongitude(), 0)); | ||||
|                 baseMarker.setPlace(explorePlace); | ||||
| 
 | ||||
|                 Glide.with(context) | ||||
|                     .asBitmap() | ||||
|  | @ -160,12 +170,15 @@ public class ExploreMapController extends MapController { | |||
|                     .into(new CustomTarget<Bitmap>() { | ||||
|                         // We add icons to markers when bitmaps are ready | ||||
|                         @Override | ||||
|                         public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { | ||||
|                             nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromBitmap( | ||||
|                                 ImageUtils.addRedBorder(resource, 6, context))); | ||||
|                             baseMarkerOptions.add(nearbyBaseMarker); | ||||
|                             if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback | ||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition); | ||||
|                         public void onResourceReady(@NonNull Bitmap resource, | ||||
|                             @Nullable Transition<? super Bitmap> transition) { | ||||
|                             baseMarker.setIcon( | ||||
|                                 ImageUtils.addRedBorder(resource, 6, context)); | ||||
|                             baseMarkerList.add(baseMarker); | ||||
|                             if (baseMarkerList.size() | ||||
|                                 == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback | ||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, | ||||
|                                     explorePlacesInfo); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|  | @ -177,20 +190,24 @@ public class ExploreMapController extends MapController { | |||
|                         @Override | ||||
|                         public void onLoadFailed(@Nullable final Drawable errorDrawable) { | ||||
|                             super.onLoadFailed(errorDrawable); | ||||
|                             nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromResource(R.drawable.image_placeholder_96)); | ||||
|                             baseMarkerOptions.add(nearbyBaseMarker); | ||||
|                             if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback | ||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition); | ||||
|                             baseMarker.fromResource(context, R.drawable.image_placeholder_96); | ||||
|                             baseMarkerList.add(baseMarker); | ||||
|                             if (baseMarkerList.size() | ||||
|                                 == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback | ||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, | ||||
|                                     explorePlacesInfo); | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|             } | ||||
|         } | ||||
|         return baseMarkerOptions; | ||||
|         return baseMarkerList; | ||||
|     } | ||||
| 
 | ||||
|     interface NearbyBaseMarkerThumbCallback { | ||||
| 
 | ||||
|         // Callback to notify thumbnails of explore markers are added as icons and ready | ||||
|         void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition); | ||||
|         void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers, | ||||
|             ExplorePlacesInfo explorePlacesInfo); | ||||
|     } | ||||
| } | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							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
	
	 Priyank Shankar
						Priyank Shankar