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 | name: Android CI | ||||||
| 
 | 
 | ||||||
| on: [push, pull_request] | on: [push, pull_request, workflow_dispatch] | ||||||
| 
 | 
 | ||||||
| concurrency: | concurrency: | ||||||
|   group: build-${{ github.event.pull_request.number || github.ref }} |   group: build-${{ github.event.pull_request.number || github.ref }} | ||||||
|  | @ -12,17 +12,17 @@ jobs: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout code |       - name: Checkout code | ||||||
|         uses: actions/checkout@v2.4.0 |         uses: actions/checkout@v3 | ||||||
| 
 | 
 | ||||||
|       - name: Set up JDK |       - name: Set up JDK | ||||||
|         uses: actions/setup-java@v2.5.0 |         uses: actions/setup-java@v3 | ||||||
|         with: |         with: | ||||||
|           distribution: "temurin" |           distribution: 'temurin' | ||||||
|           java-version: 11 |           java-version: '17' | ||||||
| 
 | 
 | ||||||
|       - name: Cache packages |       - name: Cache packages | ||||||
|         id: cache-packages |         id: cache-packages | ||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v3 | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|             ~/.gradle/caches |             ~/.gradle/caches | ||||||
|  | @ -37,7 +37,7 @@ jobs: | ||||||
| 
 | 
 | ||||||
|       - name: AVD cache |       - name: AVD cache | ||||||
|         if: github.event_name != 'pull_request' |         if: github.event_name != 'pull_request' | ||||||
|         uses: actions/cache@v2 |         uses: actions/cache@v3 | ||||||
|         id: avd-cache |         id: avd-cache | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|  | @ -89,7 +89,7 @@ jobs: | ||||||
|         run: bash ./gradlew assembleBetaDebug --stacktrace |         run: bash ./gradlew assembleBetaDebug --stacktrace | ||||||
| 
 | 
 | ||||||
|       - name: Upload betaDebug APK |       - name: Upload betaDebug APK | ||||||
|         uses: actions/upload-artifact@v2.3.1 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: betaDebugAPK |           name: betaDebugAPK | ||||||
|           path: app/build/outputs/apk/beta/debug/app-*.apk |           path: app/build/outputs/apk/beta/debug/app-*.apk | ||||||
|  | @ -98,7 +98,7 @@ jobs: | ||||||
|         run: bash ./gradlew assembleProdDebug --stacktrace |         run: bash ./gradlew assembleProdDebug --stacktrace | ||||||
| 
 | 
 | ||||||
|       - name: Upload prodDebug APK |       - name: Upload prodDebug APK | ||||||
|         uses: actions/upload-artifact@v2.3.1 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: prodDebugAPK |           name: prodDebugAPK | ||||||
|           path: app/build/outputs/apk/prod/debug/app-*.apk |           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/ | #https://docs.opencv.org/3.3.0/ | ||||||
| /libraries/opencv/javadoc/ | /libraries/opencv/javadoc/ | ||||||
| captures/* | 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="ALIGN_INIT_LIST_IN_COLUMNS" value="false" /> | ||||||
|       <option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" /> |       <option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" /> | ||||||
|     </Objective-C> |     </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> |     <Python> | ||||||
|       <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> |       <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> | ||||||
|     </Python> |     </Python> | ||||||
|     <TypeScriptCodeStyleSettings> |     <TypeScriptCodeStyleSettings> | ||||||
|       <option name="INDENT_CHAINED_CALLS" value="false" /> |       <option name="INDENT_CHAINED_CALLS" value="false" /> | ||||||
|     </TypeScriptCodeStyleSettings> |     </TypeScriptCodeStyleSettings> | ||||||
|     <XML> |     <files> | ||||||
|       <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> |       <extensions> | ||||||
|     </XML> |         <pair source="cc" header="h" fileNamingConvention="NONE" /> | ||||||
|  |         <pair source="c" header="h" fileNamingConvention="NONE" /> | ||||||
|  |       </extensions> | ||||||
|  |     </files> | ||||||
|     <codeStyleSettings language="CSS"> |     <codeStyleSettings language="CSS"> | ||||||
|       <indentOptions> |       <indentOptions> | ||||||
|         <option name="INDENT_SIZE" value="2" /> |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  | @ -318,9 +315,7 @@ | ||||||
|     <codeStyleSettings language="protobuf"> |     <codeStyleSettings language="protobuf"> | ||||||
|       <option name="RIGHT_MARGIN" value="80" /> |       <option name="RIGHT_MARGIN" value="80" /> | ||||||
|       <indentOptions> |       <indentOptions> | ||||||
|         <option name="INDENT_SIZE" value="2" /> |  | ||||||
|         <option name="CONTINUATION_INDENT_SIZE" value="2" /> |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|         <option name="TAB_SIZE" value="2" /> |  | ||||||
|       </indentOptions> |       </indentOptions> | ||||||
|     </codeStyleSettings> |     </codeStyleSettings> | ||||||
|   </code_scheme> |   </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 | # 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 | ## v4.0.3 | ||||||
| - Added "Report" button for Google UGC policy | - Added "Report" button for Google UGC policy | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							|  | @ -53,7 +53,6 @@ their contribution to the product. | ||||||
| * Butterknife | * Butterknife | ||||||
| * GSON | * GSON | ||||||
| * Timber | * Timber | ||||||
| * MapBox |  | ||||||
| 
 | 
 | ||||||
| 3rd party open source apps from which significant code has been reused: | 3rd party open source apps from which significant code has been reused: | ||||||
| * Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia | * 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]. | 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"> | <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> | <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 | ## 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] | * [User Documentation][5] | ||||||
| * [Contributor Documentation][6] | * [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/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/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/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/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/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/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/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/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/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/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). | .. 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: 'com.android.application' | ||||||
| apply plugin: 'kotlin-android' | apply plugin: 'kotlin-android' | ||||||
| apply plugin: 'kotlin-kapt' | apply plugin: 'kotlin-kapt' | ||||||
| apply plugin: 'kotlin-android-extensions' | apply plugin: 'kotlin-parcelize' | ||||||
| apply from: "$rootDir/jacoco.gradle" | apply from: "$rootDir/jacoco.gradle" | ||||||
| 
 | 
 | ||||||
| def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists() | def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists() | ||||||
|  | @ -16,13 +16,17 @@ if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
| 
 | 
 | ||||||
| dependencies { | dependencies { | ||||||
| 
 | 
 | ||||||
|     implementation project(':wikimedia-data-client') |  | ||||||
|     // Utils |     // Utils | ||||||
|     implementation 'in.yuvi:http.fluent:1.3' |     implementation 'in.yuvi:http.fluent:1.3' | ||||||
|     implementation 'com.google.code.gson:gson:2.8.5' |     implementation 'com.google.code.gson:gson:2.8.5' | ||||||
|     implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"){ |     implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){ | ||||||
|         force = true //API 19 support |         // 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 'com.squareup.okio:okio:2.2.2' | ||||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' |     implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' | ||||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.2.3' |     implementation 'io.reactivex.rxjava2:rxjava:2.2.3' | ||||||
|  | @ -45,10 +49,8 @@ dependencies { | ||||||
|     implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' |     implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' | ||||||
|     implementation 'com.dinuscxj:circleprogressbar:1.1.1' |     implementation 'com.dinuscxj:circleprogressbar:1.1.1' | ||||||
|     implementation 'com.karumi:dexter:5.0.0' |     implementation 'com.karumi:dexter:5.0.0' | ||||||
|     implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" |  | ||||||
|     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' |     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-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION" | ||||||
|     implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" |     implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" | ||||||
|     implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION" |     implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION" | ||||||
|  | @ -73,30 +75,33 @@ dependencies { | ||||||
|     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" |     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" | ||||||
|     annotationProcessor "com.google.dagger:dagger-android-processor:$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" |     implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" | ||||||
| 
 | 
 | ||||||
|     //Mocking |     //Mocking | ||||||
|     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' |     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' | ||||||
|     testImplementation 'org.mockito:mockito-inline:2.13.0' |     testImplementation 'org.mockito:mockito-inline:5.2.0' | ||||||
|     testImplementation 'org.mockito:mockito-core:2.25.1' |     testImplementation 'org.mockito:mockito-core:5.6.0' | ||||||
|     testImplementation "org.powermock:powermock-module-junit4:2.0.2" |     testImplementation "org.powermock:powermock-module-junit4:2.0.9" | ||||||
|     testImplementation "org.powermock:powermock-api-mockito2:2.0.2" |     testImplementation "org.powermock:powermock-api-mockito2:2.0.9" | ||||||
| 
 | 
 | ||||||
|     // Unit testing |     // Unit testing | ||||||
|     testImplementation 'junit:junit:4.13.2' |     testImplementation 'junit:junit:4.13.2' | ||||||
|     testImplementation 'org.robolectric:robolectric:4.6.1' |     testImplementation 'org.robolectric:robolectric:4.11.1' | ||||||
|     testImplementation 'androidx.test:core:1.4.0' |     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.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION" | ||||||
|     testImplementation "com.jraska.livedata:testing-ktx:1.1.2" |     testImplementation "com.jraska.livedata:testing-ktx:1.1.2" | ||||||
|     testImplementation "androidx.arch.core:core-testing:2.1.0" |     testImplementation "androidx.arch.core:core-testing:2.2.0" | ||||||
|     testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0" |     testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0" | ||||||
|     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0" |     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0" | ||||||
|     testImplementation 'com.facebook.soloader:soloader:0.10.1' |     testImplementation 'com.facebook.soloader:soloader:0.10.5' | ||||||
|     testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0" |     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 |     // 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-core:3.5.0-alpha04' | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' |     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04' |     androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04' | ||||||
|  | @ -119,8 +124,7 @@ dependencies { | ||||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' |     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||||
|     implementation "androidx.exifinterface:exifinterface:1.3.2" |     implementation "androidx.exifinterface:exifinterface:1.3.2" | ||||||
|     implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" |     implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" | ||||||
|     implementation "androidx.multidex:multidex:2.0.1" |     implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' | ||||||
|     compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1' |  | ||||||
| 
 | 
 | ||||||
|     //swipe_layout |     //swipe_layout | ||||||
|     implementation 'com.daimajia.swipelayout:library:1.2.0@aar' |     implementation 'com.daimajia.swipelayout:library:1.2.0@aar' | ||||||
|  | @ -131,7 +135,6 @@ dependencies { | ||||||
|     implementation "androidx.room:room-rxjava2:$ROOM_VERSION" |     implementation "androidx.room:room-rxjava2:$ROOM_VERSION" | ||||||
|     kapt "androidx.room:room-compiler:$ROOM_VERSION" |     kapt "androidx.room:room-compiler:$ROOM_VERSION" | ||||||
|     // For Kotlin use kapt instead of annotationProcessor |     // For Kotlin use kapt instead of annotationProcessor | ||||||
|     implementation 'com.squareup.retrofit2:retrofit:2.8.1' |  | ||||||
|     testImplementation "androidx.arch.core:core-testing:2.1.0" |     testImplementation "androidx.arch.core:core-testing:2.1.0" | ||||||
| 
 | 
 | ||||||
|     // Pref |     // Pref | ||||||
|  | @ -139,10 +142,12 @@ dependencies { | ||||||
|     implementation "androidx.preference:preference:$PREFERENCE_VERSION" |     implementation "androidx.preference:preference:$PREFERENCE_VERSION" | ||||||
|     // Kotlin |     // Kotlin | ||||||
|     implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" |     implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" | ||||||
|  |     //Android Media | ||||||
|  |     implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1' | ||||||
| 
 | 
 | ||||||
|     implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" |     implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" | ||||||
| 
 | 
 | ||||||
|     def work_version = "2.8.0" |     def work_version = "2.8.1" | ||||||
|     // Kotlin + coroutines |     // Kotlin + coroutines | ||||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" |     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||||
|     implementation("androidx.work:work-runtime:$work_version") |     implementation("androidx.work:work-runtime:$work_version") | ||||||
|  | @ -151,8 +156,21 @@ dependencies { | ||||||
|     //Glide |     //Glide | ||||||
|     implementation 'com.github.bumptech.glide:glide:4.12.0' |     implementation 'com.github.bumptech.glide:glide:4.12.0' | ||||||
|     annotationProcessor 'com.github.bumptech.glide:compiler: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' } |     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) { | task disableAnimations(type: Exec) { | ||||||
|  | @ -168,17 +186,17 @@ project.gradle.taskGraph.whenReady { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| android { | android { | ||||||
|     compileSdkVersion 31 |     compileSdkVersion 33 | ||||||
| 
 | 
 | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         //applicationId 'fr.free.nrw.commons' |         //applicationId 'fr.free.nrw.commons' | ||||||
| 
 | 
 | ||||||
|         versionCode 1029 |         versionCode 1040 | ||||||
|         versionName '4.0.3' |         versionName '5.0.2' | ||||||
|         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) |         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) | ||||||
| 
 | 
 | ||||||
|         minSdkVersion 21 |         minSdkVersion 21 | ||||||
|         targetSdkVersion 31 |         targetSdkVersion 33 | ||||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||||
|         testInstrumentationRunnerArguments clearPackageData: 'true' |         testInstrumentationRunnerArguments clearPackageData: 'true' | ||||||
| 
 | 
 | ||||||
|  | @ -188,17 +206,23 @@ android { | ||||||
| 
 | 
 | ||||||
|         vectorDrawables.useSupportLibrary = true |         vectorDrawables.useSupportLibrary = true | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     packagingOptions { |     packagingOptions { | ||||||
|         exclude 'META-INF/androidx.*' |         jniLibs { | ||||||
|         exclude 'META-INF/proguard/androidx-annotations.pro' |             excludes += ['META-INF/androidx.*'] | ||||||
|  |         } | ||||||
|  |         resources { | ||||||
|  |             excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro'] | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     testOptions { |     testOptions { | ||||||
|         animationsDisabled true |         animationsDisabled true | ||||||
| 
 | 
 | ||||||
|         unitTests.returnDefaultValues = true |         unitTests { | ||||||
|         unitTests.includeAndroidResources = true |             returnDefaultValues = true | ||||||
|  |             includeAndroidResources = true | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         unitTests.all { |         unitTests.all { | ||||||
|             jvmArgs '-noverify' |             jvmArgs '-noverify' | ||||||
|  | @ -223,13 +247,14 @@ android { | ||||||
|             minifyEnabled true |             minifyEnabled true | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' | ||||||
|             testProguardFile 'test-proguard-rules.txt' |             testProguardFile 'test-proguard-rules.txt' | ||||||
|  |             signingConfig signingConfigs.debug | ||||||
|             if (isRunningOnTravisAndIsNotPRBuild) { |             if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|                 signingConfig signingConfigs.release |                 signingConfig signingConfigs.release | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         debug { |         debug { | ||||||
|             minifyEnabled false |  | ||||||
|             testCoverageEnabled true |             testCoverageEnabled true | ||||||
|  |             minifyEnabled false | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' | ||||||
|             testProguardFile 'test-proguard-rules.txt' |             testProguardFile 'test-proguard-rules.txt' | ||||||
|             versionNameSuffix "-debug-" + getBranchName() |             versionNameSuffix "-debug-" + getBranchName() | ||||||
|  | @ -284,7 +309,6 @@ android { | ||||||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" |             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" |             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" |             buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" | ||||||
| 
 |  | ||||||
|             dimension 'tier' |             dimension 'tier' | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -320,20 +344,17 @@ android { | ||||||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" |             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" |             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" |             buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" | ||||||
| 
 |  | ||||||
|             dimension 'tier' |             dimension 'tier' | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     lintOptions { |  | ||||||
|         disable 'MissingTranslation' |  | ||||||
|         disable 'ExtraTranslation' |  | ||||||
|         abortOnError false |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     compileOptions { |     compileOptions { | ||||||
|         sourceCompatibility JavaVersion.VERSION_1_8 |         sourceCompatibility JavaVersion.VERSION_11 | ||||||
|         targetCompatibility JavaVersion.VERSION_1_8 |         targetCompatibility JavaVersion.VERSION_11 | ||||||
|  |     } | ||||||
|  |     kotlinOptions { | ||||||
|  |         jvmTarget = "1.8" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     buildToolsVersion buildToolsVersion |     buildToolsVersion buildToolsVersion | ||||||
|  | @ -341,7 +362,11 @@ android { | ||||||
|     buildFeatures { |     buildFeatures { | ||||||
|         viewBinding true |         viewBinding true | ||||||
|     } |     } | ||||||
| 
 |     namespace 'fr.free.nrw.commons' | ||||||
|  |     lint { | ||||||
|  |         abortOnError false | ||||||
|  |         disable 'MissingTranslation', 'ExtraTranslation' | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| String getTestUserName() { | String getTestUserName() { | ||||||
|  | @ -371,7 +396,3 @@ if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| androidExtensions { |  | ||||||
|     experimental = true |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -31,6 +31,17 @@ | ||||||
| -keepattributes Signature | -keepattributes Signature | ||||||
| # Retain declared checked exceptions for use by a Proxy instance. | # Retain declared checked exceptions for use by a Proxy instance. | ||||||
| -keepattributes Exceptions | -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 --- | # --- /Retrofit --- | ||||||
| 
 | 
 | ||||||
| # --- OkHttp + Okio --- | # --- 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, |                         .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0, | ||||||
|                                 MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"))) |                                 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()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         onView(withId(R.id.rv_descriptions)).perform( |         onView(withId(R.id.rv_descriptions)).perform( | ||||||
|  |  | ||||||
|  | @ -1,256 +1,266 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |   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.INTERNET" /> |   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||||
|     <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_SETTINGS" /> |   <uses-permission android:name="android.permission.READ_SYNC_STATS" /> | ||||||
|     <uses-permission android:name="android.permission.READ_SYNC_STATS" /> |   <uses-permission android:name="android.permission.REORDER_TASKS" /> | ||||||
|     <uses-permission android:name="android.permission.REORDER_TASKS" /> |   <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> | ||||||
|     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> |   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> |   <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> | ||||||
|     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> |   <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||||
|     <uses-permission android:name="android.permission.GET_ACCOUNTS" /> |   <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||||
|     <uses-permission android:name="android.permission.USE_CREDENTIALS" /> |   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||||
|     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> |   <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||||
|     <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> |   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> | ||||||
|     <uses-permission android:name="android.permission.SET_WALLPAPER"/> |   <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> | ||||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |   <uses-permission android:name="android.permission.SET_WALLPAPER"/> | ||||||
|     <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> |   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||||
|  |   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> | ||||||
| 
 | 
 | ||||||
|     <queries> |   <queries> | ||||||
|         <!-- Browser --> |     <!-- Browser --> | ||||||
|         <intent> |     <intent> | ||||||
|             <action android:name="android.intent.action.VIEW" /> |       <action android:name="android.intent.action.VIEW" /> | ||||||
|             <category android:name="android.intent.category.BROWSABLE" /> |       <category android:name="android.intent.category.BROWSABLE" /> | ||||||
|             <data android:scheme="https" /> |       <data android:scheme="https" /> | ||||||
|         </intent> |     </intent> | ||||||
|         <!-- Google Maps --> |     <!-- Google Maps --> | ||||||
|         <package android:name="com.google.android.apps.maps" /> |     <package android:name="com.google.android.apps.maps" /> | ||||||
|     </queries> |   </queries> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> |   <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||||
|     <uses-feature android:name="android.hardware.location.gps" /> |   <uses-feature android:name="android.hardware.location.gps" /> | ||||||
| 
 | 
 | ||||||
|     <application |   <application | ||||||
|         android:name=".CommonsApplication" |     android:name=".CommonsApplication" | ||||||
|         android:icon="@mipmap/ic_launcher" |     android:icon="@mipmap/ic_launcher" | ||||||
|         android:label="@string/app_name" |     android:label="@string/app_name" | ||||||
|         android:theme="@style/LightAppTheme" |     android:theme="@style/LightAppTheme" | ||||||
|         android:largeHeap="true" |     android:largeHeap="true" | ||||||
|         android:supportsRtl="true" |     android:supportsRtl="true" | ||||||
|         tools:replace="android:appComponentFactory" |     tools:replace="android:appComponentFactory" | ||||||
|         android:appComponentFactory="commons" |     android:appComponentFactory="commons" | ||||||
|         android:requestLegacyExternalStorage = "true" |     android:requestLegacyExternalStorage = "true" | ||||||
|         tools:ignore="GoogleAppIndexingWarning"> |     tools:ignore="GoogleAppIndexingWarning"> | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".description.DescriptionEditActivity" |       android:name=".nearby.WikidataFeedback" | ||||||
|             android:exported="true" /> |       android:exported="false" /> | ||||||
| 
 | 
 | ||||||
|         <activity android:name="org.acra.dialog.CrashReportDialog" |     <activity | ||||||
|             android:process=":acra" |       android:theme="@style/EditActivityTheme" | ||||||
|             android:launchMode="singleInstance" |       android:name=".description.DescriptionEditActivity" | ||||||
|             android:excludeFromRecents="true" |       android:exported="true" /> | ||||||
|             android:finishOnTaskLaunch="true" /> |  | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".media.ZoomableActivity" |       android:name=".edit.EditActivity" | ||||||
|             android:label="Zoomable Activity" |       android:exported="false" /> | ||||||
|             android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|             android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> |  | ||||||
| 
 | 
 | ||||||
|         <activity android:name=".auth.LoginActivity" |     <activity android:name="org.acra.dialog.CrashReportDialog" | ||||||
|                   android:exported="true"> |       android:process=":acra" | ||||||
|             <intent-filter> |       android:launchMode="singleInstance" | ||||||
|                 <category android:name="android.intent.category.LAUNCHER" /> |       android:excludeFromRecents="true" | ||||||
|  |       android:finishOnTaskLaunch="true" /> | ||||||
| 
 | 
 | ||||||
|                 <action android:name="android.intent.action.MAIN" /> |     <activity | ||||||
|             </intent-filter> |       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" |     <activity android:name=".auth.LoginActivity" | ||||||
|                 android:resource="@xml/shortcuts" /> |       android:exported="true"> | ||||||
|  |       <intent-filter> | ||||||
|  |         <category android:name="android.intent.category.LAUNCHER" /> | ||||||
| 
 | 
 | ||||||
|         </activity> |         <action android:name="android.intent.action.MAIN" /> | ||||||
|         <activity android:name=".WelcomeActivity" /> |       </intent-filter> | ||||||
| 
 | 
 | ||||||
|         <activity |       <meta-data android:name="android.app.shortcuts" | ||||||
|             android:hardwareAccelerated="false" |         android:resource="@xml/shortcuts" /> | ||||||
|             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" /> |     </activity> | ||||||
|  |     <activity android:name=".WelcomeActivity" /> | ||||||
| 
 | 
 | ||||||
|                 <data android:mimeType="image/*" /> |     <activity | ||||||
|                 <data android:mimeType="audio/ogg" /> |       android:hardwareAccelerated="false" | ||||||
|             </intent-filter> |       android:name=".upload.UploadActivity" | ||||||
|             <intent-filter android:label="@string/intent_share_upload_label"> |       android:exported="true" | ||||||
|                 <action android:name="android.intent.action.SEND_MULTIPLE" /> |       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="image/*" /> | ||||||
|                 <data android:mimeType="audio/ogg" /> |         <data android:mimeType="audio/ogg" /> | ||||||
|             </intent-filter> |       </intent-filter> | ||||||
|         </activity> |       <intent-filter android:label="@string/intent_share_upload_label"> | ||||||
|         <activity |         <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||||
|             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 |         <category android:name="android.intent.category.DEFAULT" /> | ||||||
|             android:name=".auth.SignupActivity" |  | ||||||
|             android:configChanges="orientation|screenLayout|screenSize" |  | ||||||
|             android:label="@string/title_activity_signup" /> |  | ||||||
| 
 | 
 | ||||||
|         <activity |         <data android:mimeType="image/*" /> | ||||||
|             android:name=".notification.NotificationActivity" |         <data android:mimeType="audio/ogg" /> | ||||||
|             android:label="@string/navigation_item_notification" /> |       </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" |     <activity | ||||||
|             android:label="@string/quiz"/> |       android:name=".auth.SignupActivity" | ||||||
|  |       android:configChanges="orientation|screenLayout|screenSize" | ||||||
|  |       android:label="@string/title_activity_signup" /> | ||||||
| 
 | 
 | ||||||
|         <activity android:name=".quiz.QuizResultActivity" |     <activity | ||||||
|             android:label="@string/result"/> |       android:name=".notification.NotificationActivity" | ||||||
|  |       android:label="@string/navigation_item_notification" /> | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity android:name=".quiz.QuizActivity" | ||||||
|             android:name=".customselector.ui.selector.CustomSelectorActivity" |       android:label="@string/quiz"/> | ||||||
|             android:label="@string/title_activity_custom_selector" |  | ||||||
|             android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|             android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity android:name=".quiz.QuizResultActivity" | ||||||
|             android:name=".category.CategoryDetailsActivity" |       android:label="@string/result"/> | ||||||
|             android:label="@string/title_activity_featured_images" |  | ||||||
|             android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|             android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".explore.depictions.WikidataItemDetailsActivity" |       android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||||
|             android:label="@string/title_activity_featured_images" |       android:label="@string/title_activity_custom_selector" | ||||||
|             android:configChanges="screenSize|keyboard|orientation" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|             android:parentActivityName=".contributions.MainActivity" /> |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".explore.SearchActivity" |       android:name=".category.CategoryDetailsActivity" | ||||||
|             android:label="@string/title_activity_search" |       android:label="@string/title_activity_featured_images" | ||||||
|             android:launchMode="singleTop" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|             android:configChanges="orientation|keyboardHidden|screenSize" |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
|             android:parentActivityName=".contributions.MainActivity" |  | ||||||
|             /> |  | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".profile.ProfileActivity" |       android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||||
|             android:configChanges="orientation|screenSize|keyboard" |       android:label="@string/title_activity_featured_images" | ||||||
|             android:label="@string/Profile" /> |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|             android:name=".review.ReviewActivity" |       android:name=".explore.SearchActivity" | ||||||
|             android:label="@string/title_activity_review" /> |       android:label="@string/title_activity_search" | ||||||
|  |       android:launchMode="singleTop" | ||||||
|  |       android:configChanges="orientation|keyboardHidden|screenSize" | ||||||
|  |       android:parentActivityName=".contributions.MainActivity" | ||||||
|  |       /> | ||||||
| 
 | 
 | ||||||
|         <activity |     <activity | ||||||
|           android:name=".LocationPicker.LocationPickerActivity" |       android:name=".profile.ProfileActivity" | ||||||
|           android:label="Location Picker" /> |       android:configChanges="orientation|screenSize|keyboard" | ||||||
|  |       android:label="@string/Profile" /> | ||||||
| 
 | 
 | ||||||
|         <service |     <activity | ||||||
|             android:name=".auth.WikiAccountAuthenticatorService" |       android:name=".review.ReviewActivity" | ||||||
|             android:exported="true" |       android:label="@string/title_activity_review" /> | ||||||
|             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> |  | ||||||
| 
 | 
 | ||||||
|         <service |     <activity | ||||||
|             android:name="org.acra.sender.SenderService" |       android:name=".LocationPicker.LocationPickerActivity" | ||||||
|             android:exported="false" |       android:label="Location Picker" /> | ||||||
|             android:process=":acra" /> |  | ||||||
| 
 | 
 | ||||||
|         <provider |     <service | ||||||
|             android:name=".filepicker.ExtendedFileProvider" |       android:name=".auth.WikiAccountAuthenticatorService" | ||||||
|             android:authorities="${applicationId}.provider" |       android:exported="true" | ||||||
|             android:exported="false" |       android:process=":auth"> | ||||||
|             android:grantUriPermissions="true"> |       <intent-filter> | ||||||
|             <meta-data |         <action android:name="android.accounts.AccountAuthenticator" /> | ||||||
|                 android:name="android.support.FILE_PROVIDER_PATHS" |       </intent-filter> | ||||||
|                 android:resource="@xml/provider_paths" /> |       <meta-data | ||||||
|         </provider> |         android:name="android.accounts.AccountAuthenticator" | ||||||
|  |         android:resource="@xml/authenticator" /> | ||||||
|  |     </service> | ||||||
| 
 | 
 | ||||||
|         <provider |     <service | ||||||
|             android:name=".category.CategoryContentProvider" |       android:name="org.acra.sender.SenderService" | ||||||
|             android:authorities="${applicationId}.categories.contentprovider" |       android:exported="false" | ||||||
|             android:exported="false" |       android:process=":acra" /> | ||||||
|             android:label="@string/provider_categories" |  | ||||||
|             android:syncable="false" /> |  | ||||||
| 
 | 
 | ||||||
|         <provider |     <provider | ||||||
|             android:name=".explore.recentsearches.RecentSearchesContentProvider" |       android:name=".filepicker.ExtendedFileProvider" | ||||||
|             android:authorities="${applicationId}.explore.recentsearches.contentprovider" |       android:authorities="${applicationId}.provider" | ||||||
|             android:exported="false" |       android:exported="false" | ||||||
|             android:label="@string/provider_searches" |       android:grantUriPermissions="true"> | ||||||
|             android:syncable="false" /> |       <meta-data | ||||||
|  |         android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|  |         android:resource="@xml/provider_paths" /> | ||||||
|  |     </provider> | ||||||
| 
 | 
 | ||||||
|         <provider |     <provider | ||||||
|           android:name=".recentlanguages.RecentLanguagesContentProvider" |       android:name=".category.CategoryContentProvider" | ||||||
|           android:authorities="${applicationId}.recentlanguages.contentprovider" |       android:authorities="${applicationId}.categories.contentprovider" | ||||||
|           android:exported="false" |       android:exported="false" | ||||||
|           android:label="@string/provider_recent_languages" |       android:label="@string/provider_categories" | ||||||
|           android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|         <provider |     <provider | ||||||
|             android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" |       android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||||
|             android:authorities="${applicationId}.bookmarks.contentprovider" |       android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||||
|             android:exported="false" |       android:exported="false" | ||||||
|             android:label="@string/provider_bookmarks" |       android:label="@string/provider_searches" | ||||||
|             android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|         <provider |     <provider | ||||||
|             android:name=".bookmarks.locations.BookmarkLocationsContentProvider" |       android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||||
|             android:authorities="${applicationId}.bookmarks.locations.contentprovider" |       android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||||
|             android:exported="false" |       android:exported="false" | ||||||
|             android:label="@string/provider_bookmarks_location" |       android:label="@string/provider_recent_languages" | ||||||
|             android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|         <provider |     <provider | ||||||
|           android:name=".bookmarks.items.BookmarkItemsContentProvider" |       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||||
|           android:authorities="${applicationId}.bookmarks.items.contentprovider" |       android:authorities="${applicationId}.bookmarks.contentprovider" | ||||||
|           android:exported="false" |       android:exported="false" | ||||||
|           android:label="@string/provider_bookmarks_location" |       android:label="@string/provider_bookmarks" | ||||||
|           android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|       <receiver android:name=".widget.PicOfDayAppWidget" |     <provider | ||||||
|                 android:exported="true"> |       android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||||
|             <intent-filter> |       android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||||
|                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> |       android:exported="false" | ||||||
|             </intent-filter> |       android:label="@string/provider_bookmarks_location" | ||||||
|  |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|             <meta-data |     <provider | ||||||
|                 android:name="android.appwidget.provider" |       android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||||
|                 android:resource="@xml/pic_of_day_app_widget_info" /> |       android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||||
|         </receiver> |       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> | ||||||
| 
 | 
 | ||||||
| </manifest> |     <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 static org.acra.ReportField.USER_COMMENT; | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint; | import android.annotation.SuppressLint; | ||||||
|  | import android.app.Activity; | ||||||
| import android.app.NotificationChannel; | import android.app.NotificationChannel; | ||||||
| import android.app.NotificationManager; | import android.app.NotificationManager; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
| import android.database.sqlite.SQLiteDatabase; | import android.database.sqlite.SQLiteDatabase; | ||||||
| import android.database.sqlite.SQLiteException; | import android.database.sqlite.SQLiteException; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Process; | import android.os.Process; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.multidex.BuildConfig; |  | ||||||
| import androidx.multidex.MultiDexApplication; | import androidx.multidex.MultiDexApplication; | ||||||
| import com.facebook.drawee.backends.pipeline.Fresco; | import com.facebook.drawee.backends.pipeline.Fresco; | ||||||
| import com.facebook.imagepipeline.core.ImagePipeline; | import com.facebook.imagepipeline.core.ImagePipeline; | ||||||
| import com.facebook.imagepipeline.core.ImagePipelineConfig; | import com.facebook.imagepipeline.core.ImagePipelineConfig; | ||||||
| import com.mapbox.mapboxsdk.Mapbox; | import fr.free.nrw.commons.auth.LoginActivity; | ||||||
| import com.mapbox.mapboxsdk.WellKnownTileServer; |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager; | import fr.free.nrw.commons.auth.SessionManager; | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; | 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.data.DBOpenHelper; | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import fr.free.nrw.commons.language.AppLanguageLookUpTable; | ||||||
| import fr.free.nrw.commons.logging.FileLoggingTree; | import fr.free.nrw.commons.logging.FileLoggingTree; | ||||||
| import fr.free.nrw.commons.logging.LogUtils; | import fr.free.nrw.commons.logging.LogUtils; | ||||||
| import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; | import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; | ||||||
| import fr.free.nrw.commons.settings.Prefs; | import fr.free.nrw.commons.settings.Prefs; | ||||||
| import fr.free.nrw.commons.upload.FileUtils; | import fr.free.nrw.commons.upload.FileUtils; | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils; | import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
|  | import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; | ||||||
| import io.reactivex.Completable; | import io.reactivex.Completable; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.internal.functions.Functions; | import io.reactivex.internal.functions.Functions; | ||||||
|  | @ -59,8 +61,6 @@ import org.acra.annotation.AcraCore; | ||||||
| import org.acra.annotation.AcraDialog; | import org.acra.annotation.AcraDialog; | ||||||
| import org.acra.annotation.AcraMailSender; | import org.acra.annotation.AcraMailSender; | ||||||
| import org.acra.data.StringFormat; | import org.acra.data.StringFormat; | ||||||
| import org.wikipedia.AppAdapter; |  | ||||||
| import org.wikipedia.language.AppLanguageLookUpTable; |  | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| @AcraCore( | @AcraCore( | ||||||
|  | @ -85,6 +85,9 @@ import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| public class CommonsApplication extends MultiDexApplication { | 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"; |     public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; | ||||||
|     @Inject |     @Inject | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
|  | @ -95,6 +98,9 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
|     @Named("default_preferences") |     @Named("default_preferences") | ||||||
|     JsonKvStore defaultPrefs; |     JsonKvStore defaultPrefs; | ||||||
| 
 | 
 | ||||||
|  |     @Inject | ||||||
|  |     CommonsCookieJar cookieJar; | ||||||
|  | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; |     CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; | ||||||
| 
 | 
 | ||||||
|  | @ -137,10 +143,15 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
|     ContributionDao contributionDao; |     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<>(); |     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 |      * Used to declare and initialize various components and dependencies | ||||||
|      */ |      */ | ||||||
|  | @ -150,15 +161,12 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
| 
 | 
 | ||||||
|         INSTANCE = this; |         INSTANCE = this; | ||||||
|         ACRA.init(this); |         ACRA.init(this); | ||||||
|         Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token), WellKnownTileServer.Mapbox); |  | ||||||
| 
 | 
 | ||||||
|         ApplicationlessInjection |         ApplicationlessInjection | ||||||
|             .getInstance(this) |             .getInstance(this) | ||||||
|             .getCommonsApplicationComponent() |             .getCommonsApplicationComponent() | ||||||
|             .inject(this); |             .inject(this); | ||||||
| 
 | 
 | ||||||
|         AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs)); |  | ||||||
| 
 |  | ||||||
|         initTimber(); |         initTimber(); | ||||||
| 
 | 
 | ||||||
|         if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { |         if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { | ||||||
|  | @ -286,6 +294,7 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         sessionManager.logout() |         sessionManager.logout() | ||||||
|  |             .andThen(Completable.fromAction(() -> cookieJar.clear())) | ||||||
|             .andThen(Completable.fromAction(() -> { |             .andThen(Completable.fromAction(() -> { | ||||||
|                     Timber.d("All accounts have been removed"); |                     Timber.d("All accounts have been removed"); | ||||||
|                     clearImageCache(); |                     clearImageCache(); | ||||||
|  | @ -337,4 +346,96 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
| 
 | 
 | ||||||
|         void onLogoutComplete(); |         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.app.Activity; | ||||||
| import android.content.Intent; | 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 |  * Helper class for starting the activity | ||||||
|  | @ -52,6 +53,17 @@ public final class LocationPicker { | ||||||
|           return this; |           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 |          * Gets and sets the activity | ||||||
|          * @param activity Activity |          * @param activity Activity | ||||||
|  |  | ||||||
|  | @ -1,20 +1,22 @@ | ||||||
| package fr.free.nrw.commons.LocationPicker; | 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_LOCATION; | ||||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM; | 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.content.Intent; | ||||||
| import android.graphics.BitmapFactory; | import android.content.pm.PackageManager; | ||||||
| import android.location.Location; | import android.graphics.Color; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.location.LocationManager; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
| import android.text.Html; | import android.text.Html; | ||||||
| import android.text.method.LinkMovementMethod; | import android.text.method.LinkMovementMethod; | ||||||
|  | import android.view.MotionEvent; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.Window; | import android.view.Window; | ||||||
| import android.view.animation.OvershootInterpolator; | import android.view.animation.OvershootInterpolator; | ||||||
|  | @ -28,52 +30,56 @@ import androidx.appcompat.app.ActionBar; | ||||||
| import androidx.appcompat.app.AppCompatActivity; | import androidx.appcompat.app.AppCompatActivity; | ||||||
| import androidx.appcompat.widget.AppCompatTextView; | import androidx.appcompat.widget.AppCompatTextView; | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout; | import androidx.constraintlayout.widget.ConstraintLayout; | ||||||
| import androidx.lifecycle.Observer; | import androidx.core.app.ActivityCompat; | ||||||
| import androidx.lifecycle.ViewModelProvider; | import androidx.core.content.ContextCompat; | ||||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton; | import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||||
| import com.mapbox.geojson.Point; | import fr.free.nrw.commons.CameraPosition; | ||||||
| import com.mapbox.mapboxsdk.camera.CameraPosition; | import fr.free.nrw.commons.CommonsApplication; | ||||||
| import com.mapbox.mapboxsdk.camera.CameraPosition.Builder; | import fr.free.nrw.commons.Media; | ||||||
| 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.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | 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.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.theme.BaseActivity; | ||||||
|  | import fr.free.nrw.commons.utils.DialogUtil; | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | 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.Inject; | ||||||
| import javax.inject.Named; | 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; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Helps to pick location and return the result with an intent |  * Helps to pick location and return the result with an intent | ||||||
|  */ |  */ | ||||||
| public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback, | public class LocationPickerActivity extends BaseActivity implements | ||||||
|     OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> { |     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 |      * cameraPosition : position of picker | ||||||
|      */ |      */ | ||||||
|  | @ -83,13 +89,9 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|      */ |      */ | ||||||
|     private ImageView markerImage; |     private ImageView markerImage; | ||||||
|     /** |     /** | ||||||
|      * mapboxMap : map |      * mapView : OSM Map | ||||||
|      */ |      */ | ||||||
|     private MapboxMap mapboxMap; |     private org.osmdroid.views.MapView mapView; | ||||||
|     /** |  | ||||||
|      * mapView : view of the map |  | ||||||
|      */ |  | ||||||
|     private MapView mapView; |  | ||||||
|     /** |     /** | ||||||
|      * tvAttribution : credit |      * tvAttribution : credit | ||||||
|      */ |      */ | ||||||
|  | @ -98,14 +100,14 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|      * activity : activity key |      * activity : activity key | ||||||
|      */ |      */ | ||||||
|     private String activity; |     private String activity; | ||||||
|     /** |  | ||||||
|      * location : location |  | ||||||
|      */ |  | ||||||
|     private Location location; |  | ||||||
|     /** |     /** | ||||||
|      * modifyLocationButton : button for start editing location |      * modifyLocationButton : button for start editing location | ||||||
|      */ |      */ | ||||||
|     Button modifyLocationButton; |     Button modifyLocationButton; | ||||||
|  |     /** | ||||||
|  |      * removeLocationButton : button to remove location metadata | ||||||
|  |      */ | ||||||
|  |     Button removeLocationButton; | ||||||
|     /** |     /** | ||||||
|      * showInMapButton : button for showing in map |      * showInMapButton : button for showing in map | ||||||
|      */ |      */ | ||||||
|  | @ -118,10 +120,6 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|      * fabCenterOnLocation: button for center on location; |      * fabCenterOnLocation: button for center on location; | ||||||
|      */ |      */ | ||||||
|     FloatingActionButton fabCenterOnLocation; |     FloatingActionButton fabCenterOnLocation; | ||||||
|     /** |  | ||||||
|      * droppedMarkerLayer : Layer for static screen |  | ||||||
|      */ |  | ||||||
|     private Layer droppedMarkerLayer; |  | ||||||
|     /** |     /** | ||||||
|      * shadow : imageview of shadow |      * shadow : imageview of shadow | ||||||
|      */ |      */ | ||||||
|  | @ -141,19 +139,38 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|     @Named("default_preferences") |     @Named("default_preferences") | ||||||
|     public |     public | ||||||
|     JsonKvStore applicationKvStore; |     JsonKvStore applicationKvStore; | ||||||
|  |     BasicKvStore store; | ||||||
|     /** |     /** | ||||||
|      * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly |      * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly | ||||||
|      */ |      */ | ||||||
|     @Inject |     @Inject | ||||||
|     SystemThemeUtils systemThemeUtils; |     SystemThemeUtils systemThemeUtils; | ||||||
|     private boolean isDarkTheme; |     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 |     @Override | ||||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) { |     protected void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
|         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); |         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
| 
 | 
 | ||||||
|         isDarkTheme = systemThemeUtils.isDeviceInNightMode(); |         isDarkTheme = systemThemeUtils.isDeviceInNightMode(); | ||||||
|  |         moveToCurrentLocation = false; | ||||||
|  |         store = new BasicKvStore(this, "LocationPermissions"); | ||||||
| 
 | 
 | ||||||
|         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); |         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); | ||||||
|         final ActionBar actionBar = getSupportActionBar(); |         final ActionBar actionBar = getSupportActionBar(); | ||||||
|  | @ -166,12 +183,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|             cameraPosition = getIntent() |             cameraPosition = getIntent() | ||||||
|                 .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); |                 .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); | ||||||
|             activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY); |             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(); |         bindViews(); | ||||||
|         addBackButtonListener(); |         addBackButtonListener(); | ||||||
|         addPlaceSelectedButton(); |         addPlaceSelectedButton(); | ||||||
|  | @ -179,18 +196,57 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|         getToolbarUI(); |         getToolbarUI(); | ||||||
|         addCenterOnGPSButton(); |         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)) { |         if ("UploadActivity".equals(activity)) { | ||||||
|             placeSelectedButton.setVisibility(View.GONE); |             placeSelectedButton.setVisibility(View.GONE); | ||||||
|             modifyLocationButton.setVisibility(View.VISIBLE); |             modifyLocationButton.setVisibility(View.VISIBLE); | ||||||
|  |             removeLocationButton.setVisibility(View.VISIBLE); | ||||||
|             showInMapButton.setVisibility(View.VISIBLE); |             showInMapButton.setVisibility(View.VISIBLE); | ||||||
|             largeToolbarText.setText(getResources().getString(R.string.image_location)); |             largeToolbarText.setText(getResources().getString(R.string.image_location)); | ||||||
|             smallToolbarText.setText(getResources(). |             smallToolbarText.setText(getResources(). | ||||||
|                 getString(R.string.check_whether_location_is_correct)); |                 getString(R.string.check_whether_location_is_correct)); | ||||||
|             fabCenterOnLocation.setVisibility(View.GONE); |             fabCenterOnLocation.setVisibility(View.GONE); | ||||||
|  |             markerImage.setVisibility(View.GONE); | ||||||
|  |             shadow.setVisibility(View.GONE); | ||||||
|  |             assert cameraPosition != null; | ||||||
|  |             showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(), | ||||||
|  |                 cameraPosition.getLongitude())); | ||||||
|         } |         } | ||||||
|  |         setupMapView(); | ||||||
|  |          | ||||||
|  |         if("UploadActivity".equals(activity)){ | ||||||
|  |             if(mapView != null && mapView.getController() != null && cameraPosition != null){ | ||||||
|  |                 GeoPoint cameraGeoPoint = new GeoPoint(cameraPosition.getLatitude(), | ||||||
|  |                     cameraPosition.getLongitude()); | ||||||
| 
 | 
 | ||||||
|         mapView.onCreate(savedInstanceState); |                 mapView.getController().setCenter(cameraGeoPoint); | ||||||
|         mapView.getMapAsync(this); |                 mapView.getController().animateTo(cameraGeoPoint); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -201,12 +257,26 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|         tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); |         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 |      * Clicking back button destroy locationPickerActivity | ||||||
|      */ |      */ | ||||||
|     private void addBackButtonListener() { |     private void addBackButtonListener() { | ||||||
|         final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button); |         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); |         markerImage = findViewById(R.id.location_picker_image_view_marker); | ||||||
|         tvAttribution = findViewById(R.id.tv_attribution); |         tvAttribution = findViewById(R.id.tv_attribution); | ||||||
|         modifyLocationButton = findViewById(R.id.modify_location); |         modifyLocationButton = findViewById(R.id.modify_location); | ||||||
|  |         removeLocationButton = findViewById(R.id.remove_location); | ||||||
|         showInMapButton = findViewById(R.id.show_in_map); |         showInMapButton = findViewById(R.id.show_in_map); | ||||||
|         showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); |         showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); | ||||||
|         shadow = findViewById(R.id.location_picker_image_view_shadow); |         shadow = findViewById(R.id.location_picker_image_view_shadow); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Binds the listeners |  | ||||||
|      */ |  | ||||||
|     private void bindListeners() { |  | ||||||
|         mapboxMap.addOnCameraMoveStartedListener( |  | ||||||
|             this); |  | ||||||
|         mapboxMap.addOnCameraIdleListener( |  | ||||||
|             this); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Gets toolbar color |      * Gets toolbar color | ||||||
|      */ |      */ | ||||||
|  | @ -242,49 +303,13 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|         toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); |         toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     private void setupMapView() { | ||||||
|      * Takes action when map is ready to show |         adjustCameraBasedOnOptions(); | ||||||
|      * @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(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); |         modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); | ||||||
|  |         removeLocationButton.setOnClickListener(v -> onClickRemoveLocation()); | ||||||
|         showInMapButton.setOnClickListener(v -> showInMap()); |         showInMapButton.setOnClickListener(v -> showInMap()); | ||||||
|  |         darkThemeSetup(); | ||||||
|  |         requestLocationPermissions(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -293,133 +318,70 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|     private void onClickModifyLocation() { |     private void onClickModifyLocation() { | ||||||
|         placeSelectedButton.setVisibility(View.VISIBLE); |         placeSelectedButton.setVisibility(View.VISIBLE); | ||||||
|         modifyLocationButton.setVisibility(View.GONE); |         modifyLocationButton.setVisibility(View.GONE); | ||||||
|  |         removeLocationButton.setVisibility(View.GONE); | ||||||
|         showInMapButton.setVisibility(View.GONE); |         showInMapButton.setVisibility(View.GONE); | ||||||
|         droppedMarkerLayer.setProperties(visibility(NONE)); |  | ||||||
|         markerImage.setVisibility(View.VISIBLE); |         markerImage.setVisibility(View.VISIBLE); | ||||||
|         shadow.setVisibility(View.VISIBLE); |         shadow.setVisibility(View.VISIBLE); | ||||||
|         largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); |         largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); | ||||||
|         smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); |         smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); | ||||||
|         bindListeners(); |  | ||||||
|         fabCenterOnLocation.setVisibility(View.VISIBLE); |         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 |      * Show the location in map app | ||||||
|      */ |      */ | ||||||
|     public void showInMap(){ |     public void showInMap() { | ||||||
|         Utils.handleGeoCoordinates(this, |         Utils.handleGeoCoordinates(this, | ||||||
|             new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(), |             new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(), | ||||||
|                 cameraPosition.target.getLongitude(), 0.0f)); |                 mapView.getMapCenter().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) |  | ||||||
|         )); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * move the location to the current media coordinates |      * move the location to the current media coordinates | ||||||
|      */ |      */ | ||||||
|     private void adjustCameraBasedOnOptions() { |     private void adjustCameraBasedOnOptions() { | ||||||
|         mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); |         if (cameraPosition != null) { | ||||||
|     } |             mapView.getController().setCenter(new GeoPoint(cameraPosition.getLatitude(), | ||||||
| 
 |                 cameraPosition.getLongitude())); | ||||||
|     /** |  | ||||||
|      * 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) { |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * 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 |      * Select the preferable location | ||||||
|      */ |      */ | ||||||
|  | @ -434,35 +396,127 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|     void placeSelected() { |     void placeSelected() { | ||||||
|         if (activity.equals("NoLocationUploadActivity")) { |         if (activity.equals("NoLocationUploadActivity")) { | ||||||
|             applicationKvStore.putString(LAST_LOCATION, |             applicationKvStore.putString(LAST_LOCATION, | ||||||
|                 mapboxMap.getCameraPosition().target.getLatitude() |                 mapView.getMapCenter().getLatitude() | ||||||
|                     + "," |                     + "," | ||||||
|                     + mapboxMap.getCameraPosition().target.getLongitude()); |                     + mapView.getMapCenter().getLongitude()); | ||||||
|             applicationKvStore.putString(LAST_ZOOM, mapboxMap.getCameraPosition().zoom + ""); |             applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + ""); | ||||||
|         } |         } | ||||||
|         final Intent returningIntent = new Intent(); | 
 | ||||||
|         returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, |         if (media == null) { | ||||||
|             mapboxMap.getCameraPosition()); |             final Intent returningIntent = new Intent(); | ||||||
|         setResult(AppCompatActivity.RESULT_OK, returningIntent); |             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(); |         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 |      * Center the camera on the last saved location | ||||||
|      */ |      */ | ||||||
|     private void addCenterOnGPSButton(){ |     private void addCenterOnGPSButton() { | ||||||
|         fabCenterOnLocation = findViewById(R.id.center_on_gps); |         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() { |     private void showSelectedLocationMarker(GeoPoint point) { | ||||||
|         mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(),location.getLongitude()),15.0)); |         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 |     @Override | ||||||
|     protected void onStart() { |     public void onRequestPermissionsResult(final int requestCode, | ||||||
|         super.onStart(); |         @NonNull final String[] permissions, | ||||||
|         mapView.onStart(); |         @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 |     @Override | ||||||
|  | @ -478,26 +532,102 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onStop() { |     public void onLocationPermissionDenied(String toastMessage) { | ||||||
|         super.onStop(); |         if (!ActivityCompat.shouldShowRequestPermissionRationale(this, | ||||||
|         mapView.onStop(); |             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 |     @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); |         super.onSaveInstanceState(outState); | ||||||
|         mapView.onSaveInstanceState(outState); |         if(cameraPosition!=null){ | ||||||
|     } |             outState.putParcelable(CAMERA_POS, cameraPosition); | ||||||
|  |         } | ||||||
|  |         if(activity!=null){ | ||||||
|  |             outState.putString(ACTIVITY, activity); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     @Override |         if(media!=null){ | ||||||
|     protected void onDestroy() { |             outState.putParcelable("sMedia", media); | ||||||
|         super.onDestroy(); |         } | ||||||
|         mapView.onDestroy(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onLowMemory() { |  | ||||||
|         super.onLowMemory(); |  | ||||||
|         mapView.onLowMemory(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,5 @@ | ||||||
| package fr.free.nrw.commons.LocationPicker; | package fr.free.nrw.commons.LocationPicker; | ||||||
| 
 | 
 | ||||||
| import com.mapbox.mapboxsdk.maps.Style; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Constants need for location picking |  * Constants need for location picking | ||||||
|  */ |  */ | ||||||
|  | @ -13,6 +11,9 @@ public final class LocationPickerConstants { | ||||||
|     public static final String MAP_CAMERA_POSITION |     public static final String MAP_CAMERA_POSITION | ||||||
|         = "location.picker.cameraPosition"; |         = "location.picker.cameraPosition"; | ||||||
| 
 | 
 | ||||||
|  |     public static final String MEDIA | ||||||
|  |         = "location.picker.media"; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     private LocationPickerConstants() { |     private LocationPickerConstants() { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import android.app.Application; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.lifecycle.AndroidViewModel; | import androidx.lifecycle.AndroidViewModel; | ||||||
| import androidx.lifecycle.MutableLiveData; | import androidx.lifecycle.MutableLiveData; | ||||||
| import com.mapbox.mapboxsdk.camera.CameraPosition; | import fr.free.nrw.commons.CameraPosition; | ||||||
| import org.jetbrains.annotations.NotNull; | import org.jetbrains.annotations.NotNull; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| import retrofit2.Callback; | import retrofit2.Callback; | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ public abstract class MapController { | ||||||
|     public class NearbyPlacesInfo { |     public class NearbyPlacesInfo { | ||||||
|         public List<Place> placeList; // List of nearby places |         public List<Place> placeList; // List of nearby places | ||||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area |         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 LatLng searchLatLng; // Search location for finding this places | ||||||
|         public List<Media> mediaList; // 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 class ExplorePlacesInfo { | ||||||
|         public List<Place> explorePlaceList; // List of nearby places |         public List<Place> explorePlaceList; // List of nearby places | ||||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area |         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 LatLng searchLatLng; // Search location for finding this places | ||||||
|         public List<Media> mediaList; // 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 android.os.Parcelable | ||||||
| import fr.free.nrw.commons.location.LatLng | import fr.free.nrw.commons.location.LatLng | ||||||
| import kotlinx.android.parcel.Parcelize | import kotlinx.parcelize.Parcelize | ||||||
| import org.wikipedia.dataclient.mwapi.MwQueryPage | import fr.free.nrw.commons.wikidata.model.page.PageTitle | ||||||
| import org.wikipedia.page.PageTitle |  | ||||||
| import java.util.* | import java.util.* | ||||||
| 
 | 
 | ||||||
| @Parcelize | @Parcelize | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien | ||||||
|         return Single.ambArray( |         return Single.ambArray( | ||||||
|             mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId) |             mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId) | ||||||
|                 .onErrorResumeNext { Single.never() }, |                 .onErrorResumeNext { Single.never() }, | ||||||
|             mediaClient.getMedia(media.filename) |             mediaClient.getMediaSuppressingErrors(media.filename) | ||||||
|                 .onErrorResumeNext { Single.never() } |                 .onErrorResumeNext { Single.never() } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| package fr.free.nrw.commons; | package fr.free.nrw.commons; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  | import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  | @ -15,28 +15,26 @@ import okhttp3.Response; | ||||||
| import okhttp3.ResponseBody; | import okhttp3.ResponseBody; | ||||||
| import okhttp3.logging.HttpLoggingInterceptor; | import okhttp3.logging.HttpLoggingInterceptor; | ||||||
| import okhttp3.logging.HttpLoggingInterceptor.Level; | import okhttp3.logging.HttpLoggingInterceptor.Level; | ||||||
| import org.wikipedia.dataclient.SharedPreferenceCookieManager; |  | ||||||
| import org.wikipedia.dataclient.okhttp.HttpStatusException; |  | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| public final class OkHttpConnectionFactory { | public final class OkHttpConnectionFactory { | ||||||
|     private static final String CACHE_DIR_NAME = "okhttp-cache"; |     private static final String CACHE_DIR_NAME = "okhttp-cache"; | ||||||
|     private static final long NET_CACHE_SIZE = 64 * 1024 * 1024; |     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 |     public static OkHttpClient CLIENT; | ||||||
|     private static final OkHttpClient CLIENT = createClient(); |  | ||||||
| 
 | 
 | ||||||
|     @NonNull public static OkHttpClient getClient() { |     @NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) { | ||||||
|  |         if (CLIENT == null) { | ||||||
|  |             CLIENT = createClient(cookieJar); | ||||||
|  |         } | ||||||
|         return CLIENT; |         return CLIENT; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static OkHttpClient createClient() { |     private static OkHttpClient createClient(final CommonsCookieJar cookieJar) { | ||||||
|         return new OkHttpClient.Builder() |         return new OkHttpClient.Builder() | ||||||
|                 .cookieJar(SharedPreferenceCookieManager.getInstance()) |                 .cookieJar(cookieJar) | ||||||
|                 .cache(NET_CACHE) |                 .cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null) | ||||||
|                 .connectTimeout(120, TimeUnit.SECONDS) |                 .connectTimeout(120, TimeUnit.SECONDS) | ||||||
|                 .writeTimeout(120, TimeUnit.SECONDS) |                 .writeTimeout(120, TimeUnit.SECONDS) | ||||||
|                 .readTimeout(120, TimeUnit.SECONDS) |                 .readTimeout(120, TimeUnit.SECONDS) | ||||||
|  | @ -69,6 +67,8 @@ public final class OkHttpConnectionFactory { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static class UnsuccessfulResponseInterceptor implements Interceptor { |     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( |         private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList( | ||||||
|             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"); |             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"); | ||||||
| 
 | 
 | ||||||
|  | @ -77,7 +77,16 @@ public final class OkHttpConnectionFactory { | ||||||
|         @Override |         @Override | ||||||
|         @NonNull |         @NonNull | ||||||
|         public Response intercept(@NonNull final Chain chain) throws IOException { |         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 |             // Do not intercept certain requests and let the caller handle the errors | ||||||
|             if(isExcludedUrl(chain.request())) { |             if(isExcludedUrl(chain.request())) { | ||||||
|  | @ -91,7 +100,12 @@ public final class OkHttpConnectionFactory { | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } catch (final IOException e) { |                 } 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; |                 return rsp; | ||||||
|             } |             } | ||||||
|  | @ -111,4 +125,30 @@ public final class OkHttpConnectionFactory { | ||||||
| 
 | 
 | ||||||
|     private 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.text.style.UnderlineSpan; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toast; |  | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.browser.customtabs.CustomTabColorSchemeParams; | import androidx.browser.customtabs.CustomTabColorSchemeParams; | ||||||
| import androidx.browser.customtabs.CustomTabsIntent; | import androidx.browser.customtabs.CustomTabsIntent; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import java.util.Calendar; | import java.util.Calendar; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import org.wikipedia.dataclient.WikiSite; | import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||||
| import org.wikipedia.page.PageTitle; | import fr.free.nrw.commons.wikidata.model.page.PageTitle; | ||||||
| 
 | 
 | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
| import java.util.regex.Pattern; | import java.util.regex.Pattern; | ||||||
|  | @ -31,9 +29,6 @@ import fr.free.nrw.commons.settings.Prefs; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import timber.log.Timber; | 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 class Utils { | ||||||
| 
 | 
 | ||||||
|     public static PageTitle getPageTitle(@NonNull String title) { |     public static PageTitle getPageTitle(@NonNull String title) { | ||||||
|  | @ -137,12 +132,6 @@ public class Utils { | ||||||
|      */ |      */ | ||||||
|     public static void handleWebUrl(Context context, Uri url) { |     public static void handleWebUrl(Context context, Uri url) { | ||||||
|         Timber.d("Launching web url %s", url.toString()); |         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() |         final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder() | ||||||
|             .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) |             .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package fr.free.nrw.commons; | package fr.free.nrw.commons; | ||||||
| 
 | 
 | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.text.Html; |  | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | 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 | package fr.free.nrw.commons.actions | ||||||
| 
 | 
 | ||||||
|  | import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import io.reactivex.Single | 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 |  * 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> { |     fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> { | ||||||
|         return try { |         return try { | ||||||
|             pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking) |             pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) | ||||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } |                 .map { editResponse -> | ||||||
|  |                         editResponse.edit()!!.editSucceeded() | ||||||
|  |                     } | ||||||
|         } catch (throwable: Throwable) { |         } 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> { |     fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> { | ||||||
|         return try { |         return try { | ||||||
|             pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking) |             pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) | ||||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } |                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||||
|         } catch (throwable: Throwable) { |         } 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> { |     fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> { | ||||||
|         return try { |         return try { | ||||||
|             pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking) |             pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) | ||||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } |                 .map { editResponse -> editResponse.edit()?.editSucceeded() ?: false } | ||||||
|         } catch (throwable: Throwable) { |         } 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 |      * Set new labels to Wikibase server of commons | ||||||
|      * @param summary   Edit summary |      * @param summary   Edit summary | ||||||
|  | @ -76,9 +147,14 @@ class PageEditClient( | ||||||
|                     language: String, value: String) : Observable<Int>{ |                     language: String, value: String) : Observable<Int>{ | ||||||
|         return try { |         return try { | ||||||
|             pageEditInterface.postCaptions(summary, title, language, |             pageEditInterface.postCaptions(summary, title, language, | ||||||
|                 value, csrfTokenClient.tokenBlocking).map { it.success } |                 value, csrfTokenClient.getTokenBlocking() | ||||||
|  |             ).map { it.success } | ||||||
|         } catch (throwable: Throwable) { |         } catch (throwable: Throwable) { | ||||||
|             Observable.just(0) |             if (throwable is InvalidLoginTokenException) { | ||||||
|  |                 throw throwable | ||||||
|  |             } else { | ||||||
|  |                 Observable.just(0) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -92,4 +168,4 @@ class PageEditClient( | ||||||
|             it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content() |             it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| package fr.free.nrw.commons.actions | 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.Observable | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
| import org.wikipedia.dataclient.Service | import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||||
| import org.wikipedia.dataclient.mwapi.MwQueryResponse |  | ||||||
| import org.wikipedia.edit.Edit |  | ||||||
| import org.wikipedia.wikidata.Entities |  | ||||||
| import retrofit2.http.* | import retrofit2.http.* | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -27,7 +27,7 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(Service.MW_API_PREFIX + "action=edit") |     @POST(MW_API_PREFIX + "action=edit") | ||||||
|     fun postEdit( |     fun postEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|  | @ -36,6 +36,33 @@ interface PageEditInterface { | ||||||
|         @Field("token") token: String |         @Field("token") token: String | ||||||
|     ): Observable<Edit> |     ): 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 |      * This method posts such that the Content which the page | ||||||
|      * has will be appended with the value being passed to the |      * has will be appended with the value being passed to the | ||||||
|  | @ -47,7 +74,7 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(Service.MW_API_PREFIX + "action=edit") |     @POST(MW_API_PREFIX + "action=edit") | ||||||
|     fun postAppendEdit( |     fun postAppendEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|  | @ -66,7 +93,7 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(Service.MW_API_PREFIX + "action=edit") |     @POST(MW_API_PREFIX + "action=edit") | ||||||
|     fun postPrependEdit( |     fun postPrependEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|  | @ -74,10 +101,20 @@ interface PageEditInterface { | ||||||
|         @Field("token") token: String |         @Field("token") token: String | ||||||
|     ): Observable<Edit> |     ): 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 |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @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( |     fun postCaptions( | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|  | @ -91,10 +128,7 @@ interface PageEditInterface { | ||||||
|      * @param titles : Name of the file |      * @param titles : Name of the file | ||||||
|      * @return Single<MwQueryResult> |      * @return Single<MwQueryResult> | ||||||
|      */ |      */ | ||||||
|     @GET( |     @GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=") | ||||||
|         Service.MW_API_PREFIX + |  | ||||||
|                 "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=" |  | ||||||
|     ) |  | ||||||
|     fun getWikiText( |     fun getWikiText( | ||||||
|         @Query("titles") title: String |         @Query("titles") title: String | ||||||
|     ): Single<MwQueryResponse?> |     ): Single<MwQueryResponse?> | ||||||
|  |  | ||||||
|  | @ -3,9 +3,9 @@ package fr.free.nrw.commons.actions | ||||||
| import fr.free.nrw.commons.CommonsApplication | import fr.free.nrw.commons.CommonsApplication | ||||||
| import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF | import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import org.wikipedia.csrf.CsrfTokenClient | import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | ||||||
| import org.wikipedia.dataclient.Service | import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||||
| import org.wikipedia.dataclient.mwapi.MwPostResponse | import fr.free.nrw.commons.auth.login.LoginFailedException | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Named | import javax.inject.Named | ||||||
| import javax.inject.Singleton | import javax.inject.Singleton | ||||||
|  | @ -17,7 +17,7 @@ import javax.inject.Singleton | ||||||
| @Singleton | @Singleton | ||||||
| class ThanksClient @Inject constructor( | class ThanksClient @Inject constructor( | ||||||
|     @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, |     @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 |      * Thanks a user for a particular revision | ||||||
|  | @ -26,11 +26,23 @@ class ThanksClient @Inject constructor( | ||||||
|      */ |      */ | ||||||
|     fun thank(revisionId: Long): Observable<Boolean> { |     fun thank(revisionId: Long): Observable<Boolean> { | ||||||
|         return try { |         return try { | ||||||
|             service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent) |             service.thank( | ||||||
|                 .map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 } |                 revisionId.toString(),                      // Rev | ||||||
|         } catch (throwable: Throwable) { |                 null,                                       // Log | ||||||
|             Observable.just(false) |                 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.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.view.inputmethod.InputMethodManager; | 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.ColorRes; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
|  | @ -26,31 +24,20 @@ import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.appcompat.app.AppCompatDelegate; | import androidx.appcompat.app.AppCompatDelegate; | ||||||
| import androidx.core.app.NavUtils; | import androidx.core.app.NavUtils; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| 
 | import fr.free.nrw.commons.auth.login.LoginClient; | ||||||
| import com.google.android.material.textfield.TextInputLayout; | import fr.free.nrw.commons.auth.login.LoginResult; | ||||||
| 
 | import fr.free.nrw.commons.databinding.ActivityLoginBinding; | ||||||
| import fr.free.nrw.commons.utils.ActivityUtils; | import fr.free.nrw.commons.utils.ActivityUtils; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
| import org.wikipedia.AppAdapter; | import fr.free.nrw.commons.auth.login.LoginCallback; | ||||||
| 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 java.util.Objects; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | 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.BuildConfig; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.WelcomeActivity; |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | import fr.free.nrw.commons.contributions.MainActivity; | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | @ -58,25 +45,19 @@ import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
| import retrofit2.Call; |  | ||||||
| import retrofit2.Callback; |  | ||||||
| import retrofit2.Response; |  | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| import static android.view.KeyEvent.KEYCODE_ENTER; | import static android.view.KeyEvent.KEYCODE_ENTER; | ||||||
| import static android.view.View.VISIBLE; | import static android.view.View.VISIBLE; | ||||||
| import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; | 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 { | public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
| 
 | 
 | ||||||
|     @Inject |  | ||||||
|     @Named(NAMED_COMMONS_WIKI_SITE) |  | ||||||
|     WikiSite commonsWikiSite; |  | ||||||
| 
 |  | ||||||
|     @Inject |     @Inject | ||||||
|     @Named("default_preferences") |     @Named("default_preferences") | ||||||
|     JsonKvStore applicationKvStore; |     JsonKvStore applicationKvStore; | ||||||
|  | @ -87,39 +68,16 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|     @Inject |     @Inject | ||||||
|     SystemThemeUtils systemThemeUtils; |     SystemThemeUtils systemThemeUtils; | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.login_button) |     private ActivityLoginBinding binding; | ||||||
|     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; |  | ||||||
| 
 |  | ||||||
|     ProgressDialog progressDialog; |     ProgressDialog progressDialog; | ||||||
|     private AppCompatDelegate delegate; |     private AppCompatDelegate delegate; | ||||||
|     private LoginTextWatcher textWatcher = new LoginTextWatcher(); |     private LoginTextWatcher textWatcher = new LoginTextWatcher(); | ||||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); |     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||||
|     private Call<MwQueryResponse> loginToken; |  | ||||||
|     final  String saveProgressDailog="ProgressDailog_state"; |     final  String saveProgressDailog="ProgressDailog_state"; | ||||||
|     final String saveErrorMessage ="errorMessage"; |     final String saveErrorMessage ="errorMessage"; | ||||||
|     final String saveUsername="username"; |     final String saveUsername="username"; | ||||||
|     final  String savePassword="password"; |     final  String savePassword="password"; | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(Bundle savedInstanceState) { |     public void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|  | @ -133,31 +91,50 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|         getDelegate().installViewFactory(); |         getDelegate().installViewFactory(); | ||||||
|         getDelegate().onCreate(savedInstanceState); |         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); |         binding.loginUsername.addTextChangedListener(textWatcher); | ||||||
|         passwordEdit.addTextChangedListener(textWatcher); |         binding.loginPassword.addTextChangedListener(textWatcher); | ||||||
|         twoFactorEdit.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()) { |         if (ConfigUtils.isBetaFlavour()) { | ||||||
|             loginCredentials.setText(getString(R.string.login_credential)); |             binding.loginCredentials.setText(getString(R.string.login_credential)); | ||||||
|         } else { |         } 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) { |     void onPasswordFocusChanged(View view, boolean hasFocus) { | ||||||
|         if (!hasFocus) { |         if (!hasFocus) { | ||||||
|             ViewUtil.hideKeyboard(view); |             ViewUtil.hideKeyboard(view); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnEditorAction(R.id.login_password) |     boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { | ||||||
|     boolean onEditorAction(int actionId, KeyEvent keyEvent) { |         if (binding.loginButton.isEnabled()) { | ||||||
|         if (loginButton.isEnabled()) { |  | ||||||
|             if (actionId == IME_ACTION_DONE) { |             if (actionId == IME_ACTION_DONE) { | ||||||
|                 performLogin(); |                 performLogin(); | ||||||
|                 return true; |                 return true; | ||||||
|  | @ -170,8 +147,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.skip_login) |     protected void skipLogin() { | ||||||
|     void skipLogin() { |  | ||||||
|         new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) |         new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) | ||||||
|                 .setMessage(R.string.skip_login_message) |                 .setMessage(R.string.skip_login_message) | ||||||
|                 .setCancelable(false) |                 .setCancelable(false) | ||||||
|  | @ -183,18 +159,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.forgot_password) |     protected void forgotPassword() { | ||||||
|     void forgotPassword() { |  | ||||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); |         Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.about_privacy_policy) |     protected void onPrivacyPolicyClicked() { | ||||||
|     void onPrivacyPolicyClicked() { |  | ||||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); |         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.sign_up_button) |     protected void signUp() { | ||||||
|     void signUp() { |  | ||||||
|         Intent intent = new Intent(this, SignupActivity.class); |         Intent intent = new Intent(this, SignupActivity.class); | ||||||
|         startActivity(intent); |         startActivity(intent); | ||||||
|     } |     } | ||||||
|  | @ -232,76 +205,65 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             e.printStackTrace(); |             e.printStackTrace(); | ||||||
|         } |         } | ||||||
|         usernameEdit.removeTextChangedListener(textWatcher); |         binding.loginUsername.removeTextChangedListener(textWatcher); | ||||||
|         passwordEdit.removeTextChangedListener(textWatcher); |         binding.loginPassword.removeTextChangedListener(textWatcher); | ||||||
|         twoFactorEdit.removeTextChangedListener(textWatcher); |         binding.loginTwoFactor.removeTextChangedListener(textWatcher); | ||||||
|         delegate.onDestroy(); |         delegate.onDestroy(); | ||||||
|         if(null!=loginClient) { |         if(null!=loginClient) { | ||||||
|             loginClient.cancel(); |             loginClient.cancel(); | ||||||
|         } |         } | ||||||
|  |         binding = null; | ||||||
|         super.onDestroy(); |         super.onDestroy(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.login_button) |  | ||||||
|     public void performLogin() { |     public void performLogin() { | ||||||
|         Timber.d("Login to start!"); |         Timber.d("Login to start!"); | ||||||
|         final String username = usernameEdit.getText().toString(); |         final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString(); | ||||||
|         final String rawUsername = usernameEdit.getText().toString().trim(); |         final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString(); | ||||||
|         final String password = passwordEdit.getText().toString(); |         final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString(); | ||||||
|         String twoFactorCode = twoFactorEdit.getText().toString(); |  | ||||||
| 
 | 
 | ||||||
|         showLoggingProgressBar(); |         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() { |     private void hideProgress() { | ||||||
|         progressDialog.dismiss(); |         progressDialog.dismiss(); | ||||||
|  | @ -332,13 +294,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void onLoginSuccess(LoginResult loginResult) { |     private void onLoginSuccess(LoginResult loginResult) { | ||||||
|         if (!progressDialog.isShowing()) { |  | ||||||
|             // no longer attached to activity! |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         compositeDisposable.clear(); |         compositeDisposable.clear(); | ||||||
|         sessionManager.setUserLoggedIn(true); |         sessionManager.setUserLoggedIn(true); | ||||||
|         AppAdapter.get().updateAccount(loginResult); |         sessionManager.updateAccount(loginResult); | ||||||
|         progressDialog.dismiss(); |         progressDialog.dismiss(); | ||||||
|         showSuccessAndDismissDialog(); |         showSuccessAndDismissDialog(); | ||||||
|         startMainActivity(); |         startMainActivity(); | ||||||
|  | @ -385,9 +343,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
| 
 | 
 | ||||||
|     public void askUserForTwoFactorAuth() { |     public void askUserForTwoFactorAuth() { | ||||||
|         progressDialog.dismiss(); |         progressDialog.dismiss(); | ||||||
|         twoFactorContainer.setVisibility(VISIBLE); |         binding.twoFactorContainer.setVisibility(VISIBLE); | ||||||
|         twoFactorEdit.setVisibility(VISIBLE); |         binding.loginTwoFactor.setVisibility(VISIBLE); | ||||||
|         twoFactorEdit.requestFocus(); |         binding.loginTwoFactor.requestFocus(); | ||||||
|         InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); |         InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); | ||||||
|         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); |         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); | ||||||
|         showMessageAndCancelDialog(R.string.login_failed_2fa_needed); |         showMessageAndCancelDialog(R.string.login_failed_2fa_needed); | ||||||
|  | @ -418,15 +376,15 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void showMessage(@StringRes int resId, @ColorRes int colorResId) { |     private void showMessage(@StringRes int resId, @ColorRes int colorResId) { | ||||||
|         errorMessage.setText(getString(resId)); |         binding.errorMessage.setText(getString(resId)); | ||||||
|         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); |         binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||||
|         errorMessageContainer.setVisibility(VISIBLE); |         binding.errorMessageContainer.setVisibility(VISIBLE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void showMessage(String message, @ColorRes int colorResId) { |     private void showMessage(String message, @ColorRes int colorResId) { | ||||||
|         errorMessage.setText(message); |         binding.errorMessage.setText(message); | ||||||
|         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); |         binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||||
|         errorMessageContainer.setVisibility(VISIBLE); |         binding.errorMessageContainer.setVisibility(VISIBLE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private AppCompatDelegate getDelegate() { |     private AppCompatDelegate getDelegate() { | ||||||
|  | @ -447,9 +405,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|         public void afterTextChanged(Editable editable) { |         public void afterTextChanged(Editable editable) { | ||||||
|             boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 |             boolean enabled = binding.loginUsername.getText().length() != 0 && | ||||||
|                     && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE); |                 binding.loginPassword.getText().length() != 0 && | ||||||
|             loginButton.setEnabled(enabled); |                 (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 { |         } else { | ||||||
|             outState.putBoolean(saveProgressDailog,false); |             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(saveUsername,getUsername()); // Save the username | ||||||
|         outState.putString(savePassword,getPassword()); // Save the password |         outState.putString(savePassword,getPassword()); // Save the password | ||||||
|     } |     } | ||||||
|     private String getUsername() { |     private String getUsername() { | ||||||
|         return usernameEdit.getText().toString(); |         return binding.loginUsername.getText().toString(); | ||||||
|     } |     } | ||||||
|     private String getPassword(){ |     private String getPassword(){ | ||||||
|         return  passwordEdit.getText().toString(); |         return  binding.loginPassword.getText().toString(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onRestoreInstanceState(final Bundle savedInstanceState) { |     protected void onRestoreInstanceState(final Bundle savedInstanceState) { | ||||||
|         super.onRestoreInstanceState(savedInstanceState); |         super.onRestoreInstanceState(savedInstanceState); | ||||||
|         usernameEdit.setText(savedInstanceState.getString(saveUsername)); |         binding.loginUsername.setText(savedInstanceState.getString(saveUsername)); | ||||||
|         passwordEdit.setText(savedInstanceState.getString(savePassword)); |         binding.loginPassword.setText(savedInstanceState.getString(savePassword)); | ||||||
|         if(savedInstanceState.getBoolean(saveProgressDailog)) { |         if(savedInstanceState.getBoolean(saveProgressDailog)) { | ||||||
|             performLogin(); |             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.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
| import org.wikipedia.login.LoginResult; | import fr.free.nrw.commons.auth.login.LoginResult; | ||||||
| 
 |  | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import javax.inject.Singleton; | import javax.inject.Singleton; | ||||||
|  | @ -123,18 +122,18 @@ public class SessionManager { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * 1. Clears existing accounts from account manager |      * Returns a Completable that clears existing accounts from account manager | ||||||
|      * 2. Calls MediaWikiApi's logout function to clear cookies |  | ||||||
|      * @return |  | ||||||
|      */ |      */ | ||||||
|     public Completable logout() { |     public Completable logout() { | ||||||
|         AccountManager accountManager = AccountManager.get(context); |         return Completable.fromObservable( | ||||||
|         Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); |             Observable.empty() | ||||||
|         return Completable.fromObservable(Observable.fromArray(allAccounts) |                       .doOnComplete( | ||||||
|                 .map(a -> accountManager.removeAccount(a, null, null).getResult())) |                           () -> { | ||||||
|                 .doOnComplete(() -> { |                               removeAccount(); | ||||||
|                     currentAccount = null; |                               currentAccount = null; | ||||||
|                 }); |                           } | ||||||
|  |                       ) | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| package fr.free.nrw.commons.auth; | package fr.free.nrw.commons.auth; | ||||||
| 
 | 
 | ||||||
|  | import android.content.res.Configuration; | ||||||
|  | import android.os.Build; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.webkit.WebSettings; | import android.webkit.WebSettings; | ||||||
| import android.webkit.WebView; | import android.webkit.WebView; | ||||||
|  | @ -61,4 +63,20 @@ public class SignupActivity extends BaseActivity { | ||||||
|             super.onBackPressed(); |             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.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| 
 | 
 | ||||||
| import android.widget.FrameLayout; |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| 
 |  | ||||||
| import com.google.android.material.tabs.TabLayout; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.explore.ParentViewPager; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| 
 |  | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionController; | import fr.free.nrw.commons.contributions.ContributionController; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| 
 | 
 | ||||||
|  | @ -29,12 +21,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
| 
 | 
 | ||||||
|     private FragmentManager supportFragmentManager; |     private FragmentManager supportFragmentManager; | ||||||
|     private BookmarksPagerAdapter adapter; |     private BookmarksPagerAdapter adapter; | ||||||
|     @BindView(R.id.viewPagerBookmarks) |     FragmentBookmarksBinding binding; | ||||||
|     ParentViewPager viewPager; |  | ||||||
|     @BindView(R.id.tab_layout) |  | ||||||
|     TabLayout tabLayout; |  | ||||||
|     @BindView(R.id.fragmentContainer) |  | ||||||
|     FrameLayout fragmentContainer; |  | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionController controller; |     ContributionController controller; | ||||||
|  | @ -54,7 +41,9 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setScroll(boolean canScroll) { |     public void setScroll(boolean canScroll) { | ||||||
|         viewPager.setCanScroll(canScroll); |         if (binding!=null) { | ||||||
|  |             binding.viewPagerBookmarks.setCanScroll(canScroll); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -68,8 +57,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
|         @Nullable final ViewGroup container, |         @Nullable final ViewGroup container, | ||||||
|         @Nullable final Bundle savedInstanceState) { |         @Nullable final Bundle savedInstanceState) { | ||||||
|         super.onCreateView(inflater, container, savedInstanceState); |         super.onCreateView(inflater, container, savedInstanceState); | ||||||
|         View view = inflater.inflate(R.layout.fragment_bookmarks, container, false); |         binding = FragmentBookmarksBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, view); |  | ||||||
| 
 | 
 | ||||||
|         // Activity can call methods in the fragment by acquiring a |         // Activity can call methods in the fragment by acquiring a | ||||||
|         // reference to the Fragment from FragmentManager, using findFragmentById() |         // reference to the Fragment from FragmentManager, using findFragmentById() | ||||||
|  | @ -77,14 +65,14 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
| 
 | 
 | ||||||
|         adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), |         adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), | ||||||
|             applicationKvStore.getBoolean("login_skipped")); |             applicationKvStore.getBoolean("login_skipped")); | ||||||
|         viewPager.setAdapter(adapter); |         binding.viewPagerBookmarks.setAdapter(adapter); | ||||||
|         tabLayout.setupWithViewPager(viewPager); |         binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks); | ||||||
| 
 | 
 | ||||||
|         ((MainActivity) getActivity()).showTabs(); |         ((MainActivity) getActivity()).showTabs(); | ||||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); |         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||||
| 
 | 
 | ||||||
|         setupTabLayout(); |         setupTabLayout(); | ||||||
|         return view; |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -92,15 +80,15 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
|      * visibility of tabLayout to gone. |      * visibility of tabLayout to gone. | ||||||
|      */ |      */ | ||||||
|     public void setupTabLayout() { |     public void setupTabLayout() { | ||||||
|         tabLayout.setVisibility(View.VISIBLE); |         binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|         if (adapter.getCount() == 1) { |         if (adapter.getCount() == 1) { | ||||||
|             tabLayout.setVisibility(View.GONE); |             binding.tabLayout.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public void onBackPressed() { |     public void onBackPressed() { | ||||||
|         if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition()))) |         if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition()))) | ||||||
|             .backPressed()) { |             .backPressed()) { | ||||||
|             // The event is handled internally by the adapter , no further action required. |             // The event is handled internally by the adapter , no further action required. | ||||||
|             return; |             return; | ||||||
|  | @ -108,4 +96,10 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
|         // Event is not handled by the adapter ( performed back action ) change action bar. |         // Event is not handled by the adapter ( performed back action ) change action bar. | ||||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); |         ((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.annotation.Nullable; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; | 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.CategoryImagesCallback; | ||||||
| import fr.free.nrw.commons.category.GridViewAdapter; | import fr.free.nrw.commons.category.GridViewAdapter; | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||||
| import fr.free.nrw.commons.navtab.NavTab; | import fr.free.nrw.commons.navtab.NavTab; | ||||||
|  | @ -39,8 +38,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | ||||||
|     public Fragment listFragment; |     public Fragment listFragment; | ||||||
|     private BookmarksPagerAdapter bookmarksPagerAdapter; |     private BookmarksPagerAdapter bookmarksPagerAdapter; | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.explore_container) |     FragmentFeaturedRootBinding binding; | ||||||
|     FrameLayout container; |  | ||||||
| 
 | 
 | ||||||
|     public BookmarkListRootFragment() { |     public BookmarkListRootFragment() { | ||||||
|         //empty constructor necessary otherwise crashes on recreate |         //empty constructor necessary otherwise crashes on recreate | ||||||
|  | @ -70,9 +68,8 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | ||||||
|         @Nullable final ViewGroup container, |         @Nullable final ViewGroup container, | ||||||
|         @Nullable final Bundle savedInstanceState) { |         @Nullable final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); |         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, view); |         return binding.getRoot(); | ||||||
|         return view; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -184,7 +181,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | ||||||
|     public void refreshNominatedMedia(int index) { |     public void refreshNominatedMedia(int index) { | ||||||
|         if (mediaDetails != null && !listFragment.isVisible()) { |         if (mediaDetails != null && !listFragment.isVisible()) { | ||||||
|             removeFragment(mediaDetails); |             removeFragment(mediaDetails); | ||||||
|             mediaDetails = new MediaDetailPagerFragment(false, true); |             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|             ((BookmarkFragment) getParentFragment()).setScroll(false); |             ((BookmarkFragment) getParentFragment()).setScroll(false); | ||||||
|             setFragment(mediaDetails, listFragment); |             setFragment(mediaDetails, listFragment); | ||||||
|             mediaDetails.showImage(index); |             mediaDetails.showImage(index); | ||||||
|  | @ -241,9 +238,9 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | ||||||
|     @Override |     @Override | ||||||
|     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | ||||||
|         Log.d("deneme8", "on media clicked"); |         Log.d("deneme8", "on media clicked"); | ||||||
|         container.setVisibility(View.VISIBLE); |         binding.exploreContainer.setVisibility(View.VISIBLE); | ||||||
|         ((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); |         ((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); | ||||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); |         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|         ((BookmarkFragment) getParentFragment()).setScroll(false); |         ((BookmarkFragment) getParentFragment()).setScroll(false); | ||||||
|         setFragment(mediaDetails, listFragment); |         setFragment(mediaDetails, listFragment); | ||||||
|         mediaDetails.showImage(position); |         mediaDetails.showImage(position); | ||||||
|  | @ -253,4 +250,10 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | ||||||
|     public void onBackStackChanged() { |     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 java.util.ArrayList; | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; | ||||||
| 
 | 
 | ||||||
| public class BookmarksPagerAdapter extends FragmentPagerAdapter { | public class BookmarksPagerAdapter extends FragmentPagerAdapter { | ||||||
|  |  | ||||||
|  | @ -12,10 +12,9 @@ import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
| import androidx.recyclerview.widget.RecyclerView; | import androidx.recyclerview.widget.RecyclerView; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import dagger.android.support.DaggerFragment; | import dagger.android.support.DaggerFragment; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding; | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
|  | @ -26,17 +25,7 @@ import org.jetbrains.annotations.NotNull; | ||||||
|  */ |  */ | ||||||
| public class BookmarkItemsFragment extends DaggerFragment { | public class BookmarkItemsFragment extends DaggerFragment { | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.status_message) |     private FragmentBookmarksItemsBinding binding; | ||||||
|     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; |  | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     BookmarkItemsController controller; |     BookmarkItemsController controller; | ||||||
|  | @ -51,16 +40,13 @@ public class BookmarkItemsFragment extends DaggerFragment { | ||||||
|         final ViewGroup container, |         final ViewGroup container, | ||||||
|         final Bundle savedInstanceState |         final Bundle savedInstanceState | ||||||
|     ) { |     ) { | ||||||
|         final View v = inflater.inflate(R.layout.fragment_bookmarks_items, container, false); |         binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, v); |         return binding.getRoot(); | ||||||
|         return v; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) { |     public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) { | ||||||
|         super.onViewCreated(view, savedInstanceState); |         super.onViewCreated(view, savedInstanceState); | ||||||
|         progressBar.setVisibility(View.VISIBLE); |  | ||||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); |  | ||||||
|         initList(requireContext()); |         initList(requireContext()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -77,13 +63,19 @@ public class BookmarkItemsFragment extends DaggerFragment { | ||||||
|     private void initList(final Context context) { |     private void initList(final Context context) { | ||||||
|         final List<DepictedItem> depictItems = controller.loadFavoritesItems(); |         final List<DepictedItem> depictItems = controller.loadFavoritesItems(); | ||||||
|         final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context); |         final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context); | ||||||
|         recyclerView.setAdapter(adapter); |         binding.listView.setAdapter(adapter); | ||||||
|         progressBar.setVisibility(View.GONE); |         binding.loadingImagesProgressBar.setVisibility(View.GONE); | ||||||
|         if (depictItems.isEmpty()) { |         if (depictItems.isEmpty()) { | ||||||
|             statusTextView.setText(R.string.bookmark_empty); |             binding.statusMessage.setText(R.string.bookmark_empty); | ||||||
|             statusTextView.setVisibility(View.VISIBLE); |             binding.statusMessage.setVisibility(View.VISIBLE); | ||||||
|         } else { |         } 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(); |         ContentProviderClient db = clientProvider.get(); | ||||||
|         try { |         try { | ||||||
|             cursor = db.query( |             cursor = db.query( | ||||||
|                     BookmarkLocationsContentProvider.BASE_URI, |                 BookmarkLocationsContentProvider.BASE_URI, | ||||||
|                     Table.ALL_FIELDS, |                 Table.ALL_FIELDS, | ||||||
|                     null, |                 null, | ||||||
|                     new String[]{}, |                 new String[]{}, | ||||||
|                     null); |                 null); | ||||||
|             while (cursor != null && cursor.moveToNext()) { |             while (cursor != null && cursor.moveToNext()) { | ||||||
|                 items.add(fromCursor(cursor)); |                 items.add(fromCursor(cursor)); | ||||||
|             } |             } | ||||||
|  | @ -126,11 +126,11 @@ public class BookmarkLocationsDao { | ||||||
|         ContentProviderClient db = clientProvider.get(); |         ContentProviderClient db = clientProvider.get(); | ||||||
|         try { |         try { | ||||||
|             cursor = db.query( |             cursor = db.query( | ||||||
|                     BookmarkLocationsContentProvider.BASE_URI, |                 BookmarkLocationsContentProvider.BASE_URI, | ||||||
|                     Table.ALL_FIELDS, |                 Table.ALL_FIELDS, | ||||||
|                     Table.COLUMN_NAME + "=?", |                 Table.COLUMN_NAME + "=?", | ||||||
|                     new String[]{bookmarkLocation.name}, |                 new String[]{bookmarkLocation.name}, | ||||||
|                     null); |                 null); | ||||||
|             if (cursor != null && cursor.moveToFirst()) { |             if (cursor != null && cursor.moveToFirst()) { | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|  | @ -149,7 +149,7 @@ public class BookmarkLocationsDao { | ||||||
|     @NonNull |     @NonNull | ||||||
|     Place fromCursor(final Cursor cursor) { |     Place fromCursor(final Cursor cursor) { | ||||||
|         final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), |         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(); |         final Sitelinks.Builder builder = new Sitelinks.Builder(); | ||||||
|         builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK))); |         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. |         // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. | ||||||
|         public static final String[] ALL_FIELDS = { |         public static final String[] ALL_FIELDS = { | ||||||
|                 COLUMN_NAME, |             COLUMN_NAME, | ||||||
|                 COLUMN_LANGUAGE, |             COLUMN_LANGUAGE, | ||||||
|                 COLUMN_DESCRIPTION, |             COLUMN_DESCRIPTION, | ||||||
|                 COLUMN_CATEGORY, |             COLUMN_CATEGORY, | ||||||
|                 COLUMN_LABEL_TEXT, |             COLUMN_LABEL_TEXT, | ||||||
|                 COLUMN_LABEL_ICON, |             COLUMN_LABEL_ICON, | ||||||
|                 COLUMN_LAT, |             COLUMN_LAT, | ||||||
|                 COLUMN_LONG, |             COLUMN_LONG, | ||||||
|                 COLUMN_IMAGE_URL, |             COLUMN_IMAGE_URL, | ||||||
|                 COLUMN_WIKIPEDIA_LINK, |             COLUMN_WIKIPEDIA_LINK, | ||||||
|                 COLUMN_WIKIDATA_LINK, |             COLUMN_WIKIDATA_LINK, | ||||||
|                 COLUMN_COMMONS_LINK, |             COLUMN_COMMONS_LINK, | ||||||
|                 COLUMN_PIC, |             COLUMN_PIC, | ||||||
|                 COLUMN_EXISTS, |             COLUMN_EXISTS, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; |         static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; | ||||||
| 
 | 
 | ||||||
|         static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" |         static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" | ||||||
|                 + COLUMN_NAME + " STRING PRIMARY KEY," |             + COLUMN_NAME + " STRING PRIMARY KEY," | ||||||
|                 + COLUMN_LANGUAGE + " STRING," |             + COLUMN_LANGUAGE + " STRING," | ||||||
|                 + COLUMN_DESCRIPTION + " STRING," |             + COLUMN_DESCRIPTION + " STRING," | ||||||
|                 + COLUMN_CATEGORY + " STRING," |             + COLUMN_CATEGORY + " STRING," | ||||||
|                 + COLUMN_LABEL_TEXT + " STRING," |             + COLUMN_LABEL_TEXT + " STRING," | ||||||
|                 + COLUMN_LABEL_ICON + " INTEGER," |             + COLUMN_LABEL_ICON + " INTEGER," | ||||||
|                 + COLUMN_LAT + " DOUBLE," |             + COLUMN_LAT + " DOUBLE," | ||||||
|                 + COLUMN_LONG + " DOUBLE," |             + COLUMN_LONG + " DOUBLE," | ||||||
|                 + COLUMN_IMAGE_URL + " STRING," |             + COLUMN_IMAGE_URL + " STRING," | ||||||
|                 + COLUMN_WIKIPEDIA_LINK + " STRING," |             + COLUMN_WIKIPEDIA_LINK + " STRING," | ||||||
|                 + COLUMN_WIKIDATA_LINK + " STRING," |             + COLUMN_WIKIDATA_LINK + " STRING," | ||||||
|                 + COLUMN_COMMONS_LINK + " STRING," |             + COLUMN_COMMONS_LINK + " STRING," | ||||||
|                 + COLUMN_PIC + " STRING," |             + COLUMN_PIC + " STRING," | ||||||
|                 + COLUMN_EXISTS + " STRING" |             + COLUMN_EXISTS + " STRING" | ||||||
|                 + ");"; |             + ");"; | ||||||
| 
 | 
 | ||||||
|         public static void onCreate(SQLiteDatabase db) { |         public static void onCreate(SQLiteDatabase db) { | ||||||
|             db.execSQL(CREATE_TABLE_STATEMENT); |             db.execSQL(CREATE_TABLE_STATEMENT); | ||||||
|  | @ -308,4 +308,4 @@ public class BookmarkLocationsDao { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,41 +1,57 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.locations; | package fr.free.nrw.commons.bookmarks.locations; | ||||||
| 
 | 
 | ||||||
|  | import android.Manifest.permission; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.ProgressBar; | import androidx.activity.result.ActivityResultCallback; | ||||||
| import android.widget.RelativeLayout; | import androidx.activity.result.ActivityResultLauncher; | ||||||
| import android.widget.TextView; | import androidx.activity.result.contract.ActivityResultContracts; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import dagger.android.support.DaggerFragment; | import dagger.android.support.DaggerFragment; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.contributions.ContributionController; | import fr.free.nrw.commons.contributions.ContributionController; | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding; | ||||||
| import fr.free.nrw.commons.nearby.Place; | import fr.free.nrw.commons.nearby.Place; | ||||||
| import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions; | import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions; | ||||||
| import fr.free.nrw.commons.nearby.fragments.PlaceAdapter; | import fr.free.nrw.commons.nearby.fragments.PlaceAdapter; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import kotlin.Unit; | import kotlin.Unit; | ||||||
| 
 | 
 | ||||||
| public class BookmarkLocationsFragment extends DaggerFragment { | public class BookmarkLocationsFragment extends DaggerFragment { | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.statusMessage) TextView statusTextView; |     public FragmentBookmarksLocationsBinding binding; | ||||||
|     @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; |  | ||||||
|     @BindView(R.id.listView) RecyclerView recyclerView; |  | ||||||
|     @BindView(R.id.parentLayout) RelativeLayout parentLayout; |  | ||||||
| 
 | 
 | ||||||
|     @Inject BookmarkLocationsController controller; |     @Inject BookmarkLocationsController controller; | ||||||
|     @Inject ContributionController contributionController; |     @Inject ContributionController contributionController; | ||||||
|     @Inject BookmarkLocationsDao bookmarkLocationDao; |     @Inject BookmarkLocationsDao bookmarkLocationDao; | ||||||
|     @Inject CommonPlaceClickActions commonPlaceClickActions; |     @Inject CommonPlaceClickActions commonPlaceClickActions; | ||||||
|     private PlaceAdapter adapter; |     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 |      * Create an instance of the fragment with the right bundle parameters | ||||||
|  | @ -51,25 +67,25 @@ public class BookmarkLocationsFragment extends DaggerFragment { | ||||||
|             ViewGroup container, |             ViewGroup container, | ||||||
|             Bundle savedInstanceState |             Bundle savedInstanceState | ||||||
|     ) { |     ) { | ||||||
|         View v = inflater.inflate(R.layout.fragment_bookmarks_locations, container, false); |         binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, v); |         return binding.getRoot(); | ||||||
|         return v; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { |     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||||
|         super.onViewCreated(view, savedInstanceState); |         super.onViewCreated(view, savedInstanceState); | ||||||
|         progressBar.setVisibility(View.VISIBLE); |         binding.loadingImagesProgressBar.setVisibility(View.VISIBLE); | ||||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); |         binding.listView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||||
|         adapter = new PlaceAdapter(bookmarkLocationDao, |         adapter = new PlaceAdapter(bookmarkLocationDao, | ||||||
|             place -> Unit.INSTANCE, |             place -> Unit.INSTANCE, | ||||||
|             (place, isBookmarked) -> { |             (place, isBookmarked) -> { | ||||||
|                 adapter.remove(place); |                 adapter.remove(place); | ||||||
|                 return Unit.INSTANCE; |                 return Unit.INSTANCE; | ||||||
|             }, |             }, | ||||||
|             commonPlaceClickActions |             commonPlaceClickActions, | ||||||
|  |             inAppCameraLocationPermissionLauncher | ||||||
|         ); |         ); | ||||||
|         recyclerView.setAdapter(adapter); |         binding.listView.setAdapter(adapter); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -84,12 +100,12 @@ public class BookmarkLocationsFragment extends DaggerFragment { | ||||||
|     private void initList() { |     private void initList() { | ||||||
|         List<Place> places = controller.loadFavoritesLocations(); |         List<Place> places = controller.loadFavoritesLocations(); | ||||||
|         adapter.setItems(places); |         adapter.setItems(places); | ||||||
|         progressBar.setVisibility(View.GONE); |         binding.loadingImagesProgressBar.setVisibility(View.GONE); | ||||||
|         if (places.size() <= 0) { |         if (places.size() <= 0) { | ||||||
|             statusTextView.setText(R.string.bookmark_empty); |             binding.statusMessage.setText(R.string.bookmark_empty); | ||||||
|             statusTextView.setVisibility(View.VISIBLE); |             binding.statusMessage.setVisibility(View.VISIBLE); | ||||||
|         } else { |         } 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) { |     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||||
|         contributionController.handleActivityResult(getActivity(), requestCode, resultCode, 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.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.AdapterView; | import android.widget.AdapterView; | ||||||
| import android.widget.GridView; |  | ||||||
| import android.widget.ListAdapter; | import android.widget.ListAdapter; | ||||||
| import android.widget.ProgressBar; |  | ||||||
| import android.widget.RelativeLayout; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import dagger.android.support.DaggerFragment; | import dagger.android.support.DaggerFragment; | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; | import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; | ||||||
| import fr.free.nrw.commons.category.GridViewAdapter; | 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.NetworkUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | @ -37,11 +32,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|     private GridViewAdapter gridAdapter; |     private GridViewAdapter gridAdapter; | ||||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); |     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.statusMessage) TextView statusTextView; |     private FragmentBookmarksPicturesBinding binding; | ||||||
|     @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; |  | ||||||
|     @BindView(R.id.bookmarkedPicturesList) GridView gridView; |  | ||||||
|     @BindView(R.id.parentLayout) RelativeLayout parentLayout; |  | ||||||
| 
 |  | ||||||
|     @Inject |     @Inject | ||||||
|     BookmarkPicturesController controller; |     BookmarkPicturesController controller; | ||||||
| 
 | 
 | ||||||
|  | @ -59,15 +50,14 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|             ViewGroup container, |             ViewGroup container, | ||||||
|             Bundle savedInstanceState |             Bundle savedInstanceState | ||||||
|     ) { |     ) { | ||||||
|         View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false); |         binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, v); |         return binding.getRoot(); | ||||||
|         return v; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { |     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||||
|         super.onViewCreated(view, savedInstanceState); |         super.onViewCreated(view, savedInstanceState); | ||||||
|         gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); |         binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); | ||||||
|         initList(); |         initList(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -81,13 +71,14 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|     public void onDestroy() { |     public void onDestroy() { | ||||||
|         super.onDestroy(); |         super.onDestroy(); | ||||||
|         compositeDisposable.clear(); |         compositeDisposable.clear(); | ||||||
|  |         binding = null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onResume() { |     public void onResume() { | ||||||
|         super.onResume(); |         super.onResume(); | ||||||
|         if (controller.needRefreshBookmarkedPictures()) { |         if (controller.needRefreshBookmarkedPictures()) { | ||||||
|             gridView.setVisibility(GONE); |             binding.bookmarkedPicturesList.setVisibility(GONE); | ||||||
|             if (gridAdapter != null) { |             if (gridAdapter != null) { | ||||||
|                 gridAdapter.clear(); |                 gridAdapter.clear(); | ||||||
|                 ((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged(); |                 ((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged(); | ||||||
|  | @ -107,8 +98,8 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         progressBar.setVisibility(VISIBLE); |         binding.loadingImagesProgressBar.setVisibility(VISIBLE); | ||||||
|         statusTextView.setVisibility(GONE); |         binding.statusMessage.setVisibility(GONE); | ||||||
| 
 | 
 | ||||||
|         compositeDisposable.add(controller.loadBookmarkedPictures() |         compositeDisposable.add(controller.loadBookmarkedPictures() | ||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|  | @ -120,12 +111,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|      * Handles the UI updates for no internet scenario |      * Handles the UI updates for no internet scenario | ||||||
|      */ |      */ | ||||||
|     private void handleNoInternet() { |     private void handleNoInternet() { | ||||||
|         progressBar.setVisibility(GONE); |         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||||
|             statusTextView.setVisibility(VISIBLE); |             binding.statusMessage.setVisibility(VISIBLE); | ||||||
|             statusTextView.setText(getString(R.string.no_internet)); |             binding.statusMessage.setText(getString(R.string.no_internet)); | ||||||
|         } else { |         } 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) { |     private void handleError(Throwable throwable) { | ||||||
|         Timber.e(throwable, "Error occurred while loading images inside a category"); |         Timber.e(throwable, "Error occurred while loading images inside a category"); | ||||||
|         try{ |         try{ | ||||||
|             ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images); |             ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images); | ||||||
|             initErrorView(); |             initErrorView(); | ||||||
|         }catch (Exception e){ |         }catch (Exception e){ | ||||||
|             e.printStackTrace(); |             e.printStackTrace(); | ||||||
|  | @ -147,12 +138,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|      * Handles the UI updates for a error scenario |      * Handles the UI updates for a error scenario | ||||||
|      */ |      */ | ||||||
|     private void initErrorView() { |     private void initErrorView() { | ||||||
|         progressBar.setVisibility(GONE); |         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||||
|             statusTextView.setVisibility(VISIBLE); |             binding.statusMessage.setVisibility(VISIBLE); | ||||||
|             statusTextView.setText(getString(R.string.no_images_found)); |             binding.statusMessage.setText(getString(R.string.no_images_found)); | ||||||
|         } else { |         } 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 |      * Handles the UI updates when there is no bookmarks | ||||||
|      */ |      */ | ||||||
|     private void initEmptyBookmarkListView() { |     private void initEmptyBookmarkListView() { | ||||||
|         progressBar.setVisibility(GONE); |         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |         if (gridAdapter == null || gridAdapter.isEmpty()) { | ||||||
|             statusTextView.setVisibility(VISIBLE); |             binding.statusMessage.setVisibility(VISIBLE); | ||||||
|             statusTextView.setText(getString(R.string.bookmark_empty)); |             binding.statusMessage.setText(getString(R.string.bookmark_empty)); | ||||||
|         } else { |         } else { | ||||||
|             statusTextView.setVisibility(GONE); |             binding.statusMessage.setVisibility(GONE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -188,18 +179,18 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|             setAdapter(collection); |             setAdapter(collection); | ||||||
|         } else { |         } else { | ||||||
|             if (gridAdapter.containsAll(collection)) { |             if (gridAdapter.containsAll(collection)) { | ||||||
|                 progressBar.setVisibility(GONE); |                 binding.loadingImagesProgressBar.setVisibility(GONE); | ||||||
|                 statusTextView.setVisibility(GONE); |                 binding.statusMessage.setVisibility(GONE); | ||||||
|                 gridView.setVisibility(VISIBLE); |                 binding.bookmarkedPicturesList.setVisibility(VISIBLE); | ||||||
|                 gridView.setAdapter(gridAdapter); |                 binding.bookmarkedPicturesList.setAdapter(gridAdapter); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             gridAdapter.addItems(collection); |             gridAdapter.addItems(collection); | ||||||
|             ((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged(); |             ((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged(); | ||||||
|         } |         } | ||||||
|         progressBar.setVisibility(GONE); |         binding.loadingImagesProgressBar.setVisibility(GONE); | ||||||
|         statusTextView.setVisibility(GONE); |         binding.statusMessage.setVisibility(GONE); | ||||||
|         gridView.setVisibility(VISIBLE); |         binding.bookmarkedPicturesList.setVisibility(VISIBLE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -212,7 +203,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|                 R.layout.layout_category_images, |                 R.layout.layout_category_images, | ||||||
|                 mediaList |                 mediaList | ||||||
|         ); |         ); | ||||||
|         gridView.setAdapter(gridAdapter); |         binding.bookmarkedPicturesList.setAdapter(gridAdapter); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -221,6 +212,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { | ||||||
|      * @return  GridView Adapter |      * @return  GridView Adapter | ||||||
|      */ |      */ | ||||||
|     public ListAdapter getAdapter() { |     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.content.Context; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.util.AttributeSet; | import android.util.AttributeSet; | ||||||
|  | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.campaigns.models.Campaign; | import fr.free.nrw.commons.campaigns.models.Campaign; | ||||||
|  | import fr.free.nrw.commons.databinding.LayoutCampaginBinding; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | 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.text.ParseException; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| 
 | 
 | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | import fr.free.nrw.commons.contributions.MainActivity; | ||||||
|  | @ -31,6 +29,7 @@ import fr.free.nrw.commons.utils.ViewUtil; | ||||||
|  */ |  */ | ||||||
| public class CampaignView extends SwipableCardView { | public class CampaignView extends SwipableCardView { | ||||||
|     Campaign campaign; |     Campaign campaign; | ||||||
|  |     private LayoutCampaginBinding binding; | ||||||
|     private ViewHolder viewHolder; |     private ViewHolder viewHolder; | ||||||
| 
 | 
 | ||||||
|     public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView"; |     public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView"; | ||||||
|  | @ -69,15 +68,15 @@ public class CampaignView extends SwipableCardView { | ||||||
|     @Override public boolean onSwipe(final View view) { |     @Override public boolean onSwipe(final View view) { | ||||||
|         view.setVisibility(View.GONE); |         view.setVisibility(View.GONE); | ||||||
|         ((BaseActivity) getContext()).defaultKvStore |         ((BaseActivity) getContext()).defaultKvStore | ||||||
|             .putBoolean(campaignPreference, false); |             .putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false); | ||||||
|         ViewUtil.showLongToast(getContext(), |         ViewUtil.showLongToast(getContext(), | ||||||
|             getResources().getString(R.string.nearby_campaign_dismiss_message)); |             getResources().getString(R.string.nearby_campaign_dismiss_message)); | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void init() { |     private void init() { | ||||||
|         final View rootView = inflate(getContext(), R.layout.layout_campagin, this); |         binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true); | ||||||
|         viewHolder = new ViewHolder(rootView); |         viewHolder = new ViewHolder(); | ||||||
|         setOnClickListener(view -> { |         setOnClickListener(view -> { | ||||||
|             if (campaign != null) { |             if (campaign != null) { | ||||||
|                 if (campaign.isWLMCampaign()) { |                 if (campaign.isWLMCampaign()) { | ||||||
|  | @ -90,27 +89,16 @@ public class CampaignView extends SwipableCardView { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class ViewHolder { |     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() { |         public void init() { | ||||||
|             if (campaign != null) { |             if (campaign != null) { | ||||||
|                 ivCampaign.setImageDrawable( |                 binding.ivCampaign.setImageDrawable( | ||||||
|                     getResources().getDrawable(R.drawable.ic_campaign)); |                     getResources().getDrawable(R.drawable.ic_campaign)); | ||||||
| 
 | 
 | ||||||
|                 tvTitle.setText(campaign.getTitle()); |                 binding.tvTitle.setText(campaign.getTitle()); | ||||||
|                 tvDescription.setText(campaign.getDescription()); |                 binding.tvDescription.setText(campaign.getDescription()); | ||||||
|                 try { |                 try { | ||||||
|                     if (campaign.isWLMCampaign()) { |                     if (campaign.isWLMCampaign()) { | ||||||
|                         tvDates.setText( |                         binding.tvDates.setText( | ||||||
|                             String.format("%1s - %2s", campaign.getStartDate(), |                             String.format("%1s - %2s", campaign.getStartDate(), | ||||||
|                                 campaign.getEndDate())); |                                 campaign.getEndDate())); | ||||||
|                     } else { |                     } else { | ||||||
|  | @ -118,7 +106,7 @@ public class CampaignView extends SwipableCardView { | ||||||
|                             .parse(campaign.getStartDate()); |                             .parse(campaign.getStartDate()); | ||||||
|                         final Date endDate = CommonsDateUtil.getIso8601DateFormatShort() |                         final Date endDate = CommonsDateUtil.getIso8601DateFormatShort() | ||||||
|                             .parse(campaign.getEndDate()); |                             .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))); |                             DateUtil.getExtraShortDateString(endDate))); | ||||||
|                     } |                     } | ||||||
|                 } catch (final ParseException e) { |                 } catch (final ParseException e) { | ||||||
|  |  | ||||||
|  | @ -27,30 +27,42 @@ class CategoriesModel @Inject constructor( | ||||||
|     private var selectedExistingCategories: MutableList<String> = mutableListOf() |     private var selectedExistingCategories: MutableList<String> = mutableListOf() | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns if the item contains an year |      * Returns true if an item is considered to be a spammy category which should be ignored | ||||||
|      * @param item |      * | ||||||
|  |      * @param item a category item that needs to be validated to know if it is spammy or not | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|     fun containsYear(item: String): Boolean { |     fun isSpammyCategory(item: String): Boolean { | ||||||
|         //Check for current and previous year to exclude these categories from removal |         //Check for current and previous year to exclude these categories from removal | ||||||
|         val now = Calendar.getInstance() |         val now = Calendar.getInstance() | ||||||
|         val year = now[Calendar.YEAR] |         val curYear = now[Calendar.YEAR] | ||||||
|         val yearInString = year.toString() |         val curYearInString = curYear.toString() | ||||||
|         val prevYear = year - 1 |         val prevYear = curYear - 1 | ||||||
|         val prevYearInString = prevYear.toString() |         val prevYearInString = prevYear.toString() | ||||||
|         Timber.d("Previous year: %s", prevYearInString) |         Timber.d("Previous year: %s", prevYearInString) | ||||||
| 
 | 
 | ||||||
|         //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) |         val mentionsDecade = item.matches(".*0s.*".toRegex()) | ||||||
|         //And that item does not equal the current year or previous year |         val recentDecade = item.matches(".*20[0-2]0s.*".toRegex()) | ||||||
|         //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) |         val spammyCategory = item.matches("(.*)needing(.*)".toRegex()) | ||||||
|         //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 |                           || item.matches("(.*)taken on(.*)".toRegex()) | ||||||
|         return item.matches(".*(19|20)\\d{2}.*".toRegex()) | 
 | ||||||
|                 && !item.contains(yearInString) |         // always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750) | ||||||
|                 && !item.contains(prevYearInString) |         if (spammyCategory) { | ||||||
|                 || item.matches("(.*)needing(.*)".toRegex()) |             return true | ||||||
|                 || item.matches("(.*)taken on(.*)".toRegex()) |         } | ||||||
|                 || item.matches(".*0s.*".toRegex()) | 
 | ||||||
|                 && !item.matches(".*(200|201)0s.*".toRegex()) |         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) |         return Observable.fromIterable(categoryNames) | ||||||
|             .map { categoryName -> |             .map { categoryName -> | ||||||
|                 buildCategories(categoryName) |                 buildCategories(categoryName) | ||||||
|             }.toList().toObservable() |             } | ||||||
|  |             .filter { categoryItem -> | ||||||
|  |                 categoryItem.name != "Hidden" | ||||||
|  |             } | ||||||
|  |             .toList().toObservable() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| package fr.free.nrw.commons.category | package fr.free.nrw.commons.category | ||||||
| 
 | 
 | ||||||
| import io.reactivex.Single | 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.Inject | ||||||
| import javax.inject.Singleton | import javax.inject.Singleton | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,13 +15,12 @@ import androidx.appcompat.widget.Toolbar; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import androidx.viewpager.widget.ViewPager; | import androidx.viewpager.widget.ViewPager; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import com.google.android.material.tabs.TabLayout; | import com.google.android.material.tabs.TabLayout; | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.ViewPagerAdapter; | import fr.free.nrw.commons.ViewPagerAdapter; | ||||||
|  | import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding; | ||||||
| import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; | import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; | ||||||
| import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment; | import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment; | ||||||
| import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment; | import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment; | ||||||
|  | @ -29,7 +28,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | 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 |  * This activity displays details of a particular category | ||||||
|  | @ -45,23 +44,23 @@ public class CategoryDetailsActivity extends BaseActivity | ||||||
|     private CategoriesMediaFragment categoriesMediaFragment; |     private CategoriesMediaFragment categoriesMediaFragment; | ||||||
|     private MediaDetailPagerFragment mediaDetails; |     private MediaDetailPagerFragment mediaDetails; | ||||||
|     private String categoryName; |     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; |     ViewPagerAdapter viewPagerAdapter; | ||||||
| 
 | 
 | ||||||
|  |     private ActivityCategoryDetailsBinding binding; | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(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(); |         supportFragmentManager = getSupportFragmentManager(); | ||||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); |         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||||
|         viewPager.setAdapter(viewPagerAdapter); |         binding.viewPager.setAdapter(viewPagerAdapter); | ||||||
|         viewPager.setOffscreenPageLimit(2); |         binding.viewPager.setOffscreenPageLimit(2); | ||||||
|         tabLayout.setupWithViewPager(viewPager); |         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||||
|         setSupportActionBar(toolbar); |         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|         setTabs(); |         setTabs(); | ||||||
|         setPageTitle(); |         setPageTitle(); | ||||||
|  | @ -110,12 +109,12 @@ public class CategoryDetailsActivity extends BaseActivity | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void onMediaClicked(int position) { |     public void onMediaClicked(int position) { | ||||||
|         tabLayout.setVisibility(View.GONE); |         binding.tabLayout.setVisibility(View.GONE); | ||||||
|         viewPager.setVisibility(View.GONE); |         binding.viewPager.setVisibility(View.GONE); | ||||||
|         mediaContainer.setVisibility(View.VISIBLE); |         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||||
|         if (mediaDetails == null || !mediaDetails.isVisible()) { |         if (mediaDetails == null || !mediaDetails.isVisible()) { | ||||||
|             // set isFeaturedImage true for featured images, to include author field on media detail |             // 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(); |             FragmentManager supportFragmentManager = getSupportFragmentManager(); | ||||||
|             supportFragmentManager |             supportFragmentManager | ||||||
|                     .beginTransaction() |                     .beginTransaction() | ||||||
|  | @ -216,9 +215,9 @@ public class CategoryDetailsActivity extends BaseActivity | ||||||
|     @Override |     @Override | ||||||
|     public void onBackPressed() { |     public void onBackPressed() { | ||||||
|         if (supportFragmentManager.getBackStackEntryCount() == 1){ |         if (supportFragmentManager.getBackStackEntryCount() == 1){ | ||||||
|             tabLayout.setVisibility(View.VISIBLE); |             binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|             viewPager.setVisibility(View.VISIBLE); |             binding.viewPager.setVisibility(View.VISIBLE); | ||||||
|             mediaContainer.setVisibility(View.GONE); |             binding.mediaContainer.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|         super.onBackPressed(); |         super.onBackPressed(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_E | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.util.Log; |  | ||||||
| import fr.free.nrw.commons.BuildConfig; | import fr.free.nrw.commons.BuildConfig; | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | 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 | package fr.free.nrw.commons.category | ||||||
| 
 | 
 | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
| import kotlinx.android.parcel.Parcelize | import kotlinx.parcelize.Parcelize | ||||||
| 
 | 
 | ||||||
| @Parcelize | @Parcelize | ||||||
| data class CategoryItem(val name: String, val description: String?, | data class CategoryItem(val name: String, val description: String?, | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import android.os.Parcelable | ||||||
| import androidx.room.Embedded | import androidx.room.Embedded | ||||||
| import androidx.room.Entity | import androidx.room.Entity | ||||||
| import androidx.room.PrimaryKey | import androidx.room.PrimaryKey | ||||||
|  | import fr.free.nrw.commons.CommonsApplication | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.upload.UploadItem | 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 | ||||||
| import fr.free.nrw.commons.upload.WikidataPlace.Companion.from | import fr.free.nrw.commons.upload.WikidataPlace.Companion.from | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
| import kotlinx.android.parcel.Parcelize | import kotlinx.parcelize.Parcelize | ||||||
| import java.util.* | import java.io.File | ||||||
|  | import java.util.Date | ||||||
| 
 | 
 | ||||||
| @Entity(tableName = "contribution") | @Entity(tableName = "contribution") | ||||||
| @Parcelize | @Parcelize | ||||||
|  | @ -43,7 +45,11 @@ data class Contribution constructor( | ||||||
|     var hasInvalidLocation : Int =  0, |     var hasInvalidLocation : Int =  0, | ||||||
|     var contentUri: Uri? = null, |     var contentUri: Uri? = null, | ||||||
|     var countryCode : String? = 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 { | ) : Parcelable { | ||||||
| 
 | 
 | ||||||
|     fun completeWith(media: Media): Contribution { |     fun completeWith(media: Media): Contribution { | ||||||
|  | @ -111,6 +117,21 @@ data class Contribution constructor( | ||||||
|          */ |          */ | ||||||
|         fun formatDescriptions(descriptions: List<UploadMediaDetail>) = |         fun formatDescriptions(descriptions: List<UploadMediaDetail>) = | ||||||
|             descriptions.filter { it.descriptionText.isNotEmpty() } |             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 static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; | ||||||
| 
 | 
 | ||||||
| import android.Manifest; |  | ||||||
| import android.Manifest.permission; | import android.Manifest.permission; | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.os.Build.VERSION; | import android.widget.Toast; | ||||||
| import android.os.Build.VERSION_CODES; | import androidx.activity.result.ActivityResultLauncher; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.filepicker.DefaultCallback; | 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.FilePicker.ImageSource; | ||||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | import fr.free.nrw.commons.filepicker.UploadableFile; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | 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.nearby.Place; | ||||||
| import fr.free.nrw.commons.upload.UploadActivity; | 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.PermissionUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | @ -31,6 +35,13 @@ public class ContributionController { | ||||||
| 
 | 
 | ||||||
|     public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; |     public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; | ||||||
|     private final JsonKvStore defaultKvStore; |     private final JsonKvStore defaultKvStore; | ||||||
|  |     private LatLng locationBeforeImageCapture; | ||||||
|  |     private boolean isInAppCameraUpload; | ||||||
|  |     public LocationPermissionCallback locationPermissionCallback; | ||||||
|  |     private LocationPermissionsHelper locationPermissionsHelper; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     LocationServiceManager locationManager; | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { |     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { | ||||||
|  | @ -40,7 +51,8 @@ public class ContributionController { | ||||||
|     /** |     /** | ||||||
|      * Check for permissions and initiate camera click |      * 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); |         boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||||
|         if (!useExtStorage) { |         if (!useExtStorage) { | ||||||
|             initiateCameraUpload(activity); |             initiateCameraUpload(activity); | ||||||
|  | @ -48,10 +60,133 @@ public class ContributionController { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, |         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE, |             () -> { | ||||||
|                 () -> initiateCameraUpload(activity), |                 if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { | ||||||
|                 R.string.storage_permission_title, |                     defaultKvStore.putBoolean("inAppCameraFirstRun", false); | ||||||
|                 R.string.write_storage_permission_rationale); |                     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 |      * Initiate gallery picker with permission | ||||||
|      */ |      */ | ||||||
|     public void initiateCustomGalleryPickWithPermission(final Activity activity) { |     public void initiateCustomGalleryPickWithPermission(final Activity activity) { | ||||||
|         setPickerConfiguration(activity,true); |         setPickerConfiguration(activity, true); | ||||||
| 
 | 
 | ||||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, |         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||||
|             Manifest.permission.WRITE_EXTERNAL_STORAGE, |             () -> FilePicker.openCustomSelector(activity, 0), | ||||||
|             () -> { |  | ||||||
|                 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); |  | ||||||
|             }, |  | ||||||
|             R.string.storage_permission_title, |             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 |      * 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); |         setPickerConfiguration(activity, allowMultipleUploads); | ||||||
|         FilePicker.openGallery(activity, 0); |         boolean openDocumentIntentPreferred = defaultKvStore.getBoolean( | ||||||
|  |             "openDocumentPhotoPickerPref", true); | ||||||
|  |         FilePicker.openGallery(activity, 0, openDocumentIntentPreferred); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Sets configuration for file picker |      * Sets configuration for file picker | ||||||
|      */ |      */ | ||||||
|     private void setPickerConfiguration(Activity activity, |     private void setPickerConfiguration(Activity activity, | ||||||
|                                         boolean allowMultipleUploads) { |         boolean allowMultipleUploads) { | ||||||
|         boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true); |         boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||||
|         FilePicker.configuration(activity) |         FilePicker.configuration(activity) | ||||||
|                 .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) |             .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) | ||||||
|                 .setAllowMultiplePickInGallery(allowMultipleUploads); |             .setAllowMultiplePickInGallery(allowMultipleUploads); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -110,42 +237,50 @@ public class ContributionController { | ||||||
|      */ |      */ | ||||||
|     private void initiateCameraUpload(Activity activity) { |     private void initiateCameraUpload(Activity activity) { | ||||||
|         setPickerConfiguration(activity, false); |         setPickerConfiguration(activity, false); | ||||||
|  |         if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { | ||||||
|  |             locationBeforeImageCapture = locationManager.getLastLocation(); | ||||||
|  |         } | ||||||
|  |         isInAppCameraUpload = true; | ||||||
|         FilePicker.openCameraForImage(activity, 0); |         FilePicker.openCameraForImage(activity, 0); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Attaches callback for file picker. |      * Attaches callback for file picker. | ||||||
|      */ |      */ | ||||||
|     public void handleActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { |     public void handleActivityResult(Activity activity, int requestCode, int resultCode, | ||||||
|         FilePicker.handleActivityResult(requestCode, resultCode, data, activity, new DefaultCallback() { |         Intent data) { | ||||||
|  |         FilePicker.handleActivityResult(requestCode, resultCode, data, activity, | ||||||
|  |             new DefaultCallback() { | ||||||
| 
 | 
 | ||||||
|             @Override |                 @Override | ||||||
|             public void onCanceled(final ImageSource source, final int type) { |                 public void onCanceled(final ImageSource source, final int type) { | ||||||
|                 super.onCanceled(source, type); |                     super.onCanceled(source, type); | ||||||
|                 defaultKvStore.remove(PLACE_OBJECT); |                     defaultKvStore.remove(PLACE_OBJECT); | ||||||
|             } |                 } | ||||||
| 
 | 
 | ||||||
|             @Override |                 @Override | ||||||
|             public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) { |                 public void onImagePickerError(Exception e, FilePicker.ImageSource source, | ||||||
|                 ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); |                     int type) { | ||||||
|             } |                     ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|             @Override |                 @Override | ||||||
|             public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) { |                 public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, | ||||||
|                 Intent intent = handleImagesPicked(activity, imagesFiles); |                     FilePicker.ImageSource source, int type) { | ||||||
|                 activity.startActivity(intent); |                     Intent intent = handleImagesPicked(activity, imagesFiles); | ||||||
|             } |                     activity.startActivity(intent); | ||||||
|         }); |                 } | ||||||
|  |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public List<UploadableFile> handleExternalImagesPicked(Activity activity, |     public List<UploadableFile> handleExternalImagesPicked(Activity activity, | ||||||
|                                                            Intent data) { |         Intent data) { | ||||||
|         return FilePicker.handleExternalImagesPicked(data, activity); |         return FilePicker.handleExternalImagesPicked(data, activity); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns intent to be passed to upload activity |      * Returns intent to be passed to upload activity Attaches place object for nearby uploads and | ||||||
|      * Attaches place object for nearby uploads |      * location before image capture if in-app camera is used | ||||||
|      */ |      */ | ||||||
|     private Intent handleImagesPicked(Context context, |     private Intent handleImagesPicked(Context context, | ||||||
|         List<UploadableFile> imagesFiles) { |         List<UploadableFile> imagesFiles) { | ||||||
|  | @ -159,7 +294,17 @@ public class ContributionController { | ||||||
|             shareIntent.putExtra(PLACE_OBJECT, place); |             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; |         return shareIntent; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,7 +12,6 @@ import androidx.room.Update; | ||||||
| import io.reactivex.Completable; | import io.reactivex.Completable; | ||||||
| import io.reactivex.Single; | import io.reactivex.Single; | ||||||
| import java.util.Calendar; | import java.util.Calendar; | ||||||
| import java.util.Date; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
| @Dao | @Dao | ||||||
|  |  | ||||||
|  | @ -12,14 +12,12 @@ import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.AlertDialog; | import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.appcompat.app.AlertDialog.Builder; | import androidx.appcompat.app.AlertDialog.Builder; | ||||||
| import androidx.recyclerview.widget.RecyclerView; | import androidx.recyclerview.widget.RecyclerView; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import butterknife.OnClick; |  | ||||||
| import com.facebook.drawee.view.SimpleDraweeView; | import com.facebook.drawee.view.SimpleDraweeView; | ||||||
| import com.facebook.imagepipeline.request.ImageRequest; | import com.facebook.imagepipeline.request.ImageRequest; | ||||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder; | import com.facebook.imagepipeline.request.ImageRequestBuilder; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | ||||||
|  | import fr.free.nrw.commons.databinding.LayoutContributionBinding; | ||||||
| import fr.free.nrw.commons.media.MediaClient; | import fr.free.nrw.commons.media.MediaClient; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | @ -29,29 +27,8 @@ import java.io.File; | ||||||
| public class ContributionViewHolder extends RecyclerView.ViewHolder { | public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
| 
 | 
 | ||||||
|     private final Callback callback; |     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 int position; | ||||||
|     private Contribution contribution; |     private Contribution contribution; | ||||||
|  | @ -67,9 +44,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|         super(parent); |         super(parent); | ||||||
|         this.parent = parent; |         this.parent = parent; | ||||||
|         this.mediaClient = mediaClient; |         this.mediaClient = mediaClient; | ||||||
|         ButterKnife.bind(this, parent); |  | ||||||
|         this.callback = callback; |         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 |         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||||
|         an upload might take a dozen seconds. */ |         an upload might take a dozen seconds. */ | ||||||
|         AlertDialog.Builder builder = new Builder(parent.getContext()); |         AlertDialog.Builder builder = new Builder(parent.getContext()); | ||||||
|  | @ -87,14 +71,17 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
| 
 | 
 | ||||||
|         this.contribution = contribution; |         this.contribution = contribution; | ||||||
|         this.position = position; |         this.position = position; | ||||||
|         titleView.setText(contribution.getMedia().getMostRelevantCaption()); |         binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption()); | ||||||
|         authorView.setText(contribution.getMedia().getAuthor()); |         binding.authorView.setText(contribution.getMedia().getAuthor()); | ||||||
| 
 | 
 | ||||||
|         //Removes flicker of loading image. |         //Removes flicker of loading image. | ||||||
|         imageView.getHierarchy().setFadeDuration(0); |         binding.contributionImage.getHierarchy().setFadeDuration(0); | ||||||
| 
 | 
 | ||||||
|         imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); |         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||||
|         imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder); |         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||||
|  |          | ||||||
|  |          | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
|         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), |         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), | ||||||
|             contribution.getLocalUri()); |             contribution.getLocalUri()); | ||||||
|  | @ -103,73 +90,77 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) |                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||||
|                     .setProgressiveRenderingEnabled(true) |                     .setProgressiveRenderingEnabled(true) | ||||||
|                     .build(); |                     .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); |                 final File file = new File(imageSource); | ||||||
|                 imageRequest = ImageRequest.fromFile(file); |                 imageRequest = ImageRequest.fromFile(file); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if(imageRequest != null){ |             if(imageRequest != null){ | ||||||
|                 imageView.setImageRequest(imageRequest); |                 binding.contributionImage.setImageRequest(imageRequest); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         seqNumView.setText(String.valueOf(position + 1)); |         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); | ||||||
|         seqNumView.setVisibility(View.VISIBLE); |         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); | ||||||
| 
 | 
 | ||||||
|         addToWikipediaButton.setVisibility(View.GONE); |         binding.wikipediaButton.setVisibility(View.GONE); | ||||||
|         switch (contribution.getState()) { |         switch (contribution.getState()) { | ||||||
|             case Contribution.STATE_COMPLETED: |             case Contribution.STATE_COMPLETED: | ||||||
|                 stateView.setVisibility(View.GONE); |                 binding.contributionState.setVisibility(View.GONE); | ||||||
|                 progressView.setVisibility(View.GONE); |                 binding.contributionProgress.setVisibility(View.GONE); | ||||||
|                 imageOptions.setVisibility(View.GONE); |                 binding.imageOptions.setVisibility(View.GONE); | ||||||
|                 stateView.setText(""); |                 binding.contributionState.setText(""); | ||||||
|                 checkIfMediaExistsOnWikipediaPage(contribution); |                 checkIfMediaExistsOnWikipediaPage(contribution); | ||||||
|                 break; |                 break; | ||||||
|             case Contribution.STATE_QUEUED: |             case Contribution.STATE_QUEUED: | ||||||
|             case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE: |             case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE: | ||||||
|                 progressView.setVisibility(View.GONE); |                 binding.contributionProgress.setVisibility(View.GONE); | ||||||
|                 stateView.setVisibility(View.VISIBLE); |                 binding.contributionState.setVisibility(View.VISIBLE); | ||||||
|                 stateView.setText(R.string.contribution_state_queued); |                 binding.contributionState.setText(R.string.contribution_state_queued); | ||||||
|                 imageOptions.setVisibility(View.GONE); |                 binding.imageOptions.setVisibility(View.GONE); | ||||||
|                 break; |                 break; | ||||||
|             case Contribution.STATE_IN_PROGRESS: |             case Contribution.STATE_IN_PROGRESS: | ||||||
|                 stateView.setVisibility(View.GONE); |                 binding.contributionState.setVisibility(View.GONE); | ||||||
|                 progressView.setVisibility(View.VISIBLE); |                 binding.contributionProgress.setVisibility(View.VISIBLE); | ||||||
|                 addToWikipediaButton.setVisibility(View.GONE); |                 binding.wikipediaButton.setVisibility(View.GONE); | ||||||
|                 pauseResumeButton.setVisibility(View.VISIBLE); |                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||||
|                 cancelButton.setVisibility(View.GONE); |                 binding.cancelButton.setVisibility(View.GONE); | ||||||
|                 retryButton.setVisibility(View.GONE); |                 binding.retryButton.setVisibility(View.GONE); | ||||||
|                 imageOptions.setVisibility(View.VISIBLE); |                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||||
|                 final long total = contribution.getDataLength(); |                 final long total = contribution.getDataLength(); | ||||||
|                 final long transferred = contribution.getTransferred(); |                 final long transferred = contribution.getTransferred(); | ||||||
|                 if (transferred == 0 || transferred >= total) { |                 if (transferred == 0 || transferred >= total) { | ||||||
|                     progressView.setIndeterminate(true); |                     binding.contributionProgress.setIndeterminate(true); | ||||||
|                 } else { |                 } else { | ||||||
|                     progressView.setIndeterminate(false); |                     binding.contributionProgress.setIndeterminate(false); | ||||||
|                     progressView.setProgress((int) (((double) transferred / (double) total) * 100)); |                     binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100)); | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
|             case Contribution.STATE_PAUSED: |             case Contribution.STATE_PAUSED: | ||||||
|                 progressView.setVisibility(View.GONE); |                 binding.contributionProgress.setVisibility(View.GONE); | ||||||
|                 stateView.setVisibility(View.VISIBLE); |                 binding.contributionState.setVisibility(View.VISIBLE); | ||||||
|                 stateView.setText(R.string.paused); |                 binding.contributionState.setText(R.string.paused); | ||||||
|                 cancelButton.setVisibility(View.VISIBLE); |                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||||
|                 retryButton.setVisibility(View.GONE); |                 binding.retryButton.setVisibility(View.GONE); | ||||||
|                 pauseResumeButton.setVisibility(View.VISIBLE); |                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||||
|                 imageOptions.setVisibility(View.VISIBLE); |                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||||
|                 setResume(); |                 setResume(); | ||||||
|                 if(pausingPopUp.isShowing()){ |                 if(pausingPopUp.isShowing()){ | ||||||
|                     pausingPopUp.hide(); |                     pausingPopUp.hide(); | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
|             case Contribution.STATE_FAILED: |             case Contribution.STATE_FAILED: | ||||||
|                 stateView.setVisibility(View.VISIBLE); |                 binding.contributionState.setVisibility(View.VISIBLE); | ||||||
|                 stateView.setText(R.string.contribution_state_failed); |                 binding.contributionState.setText(R.string.contribution_state_failed); | ||||||
|                 progressView.setVisibility(View.GONE); |                 binding.contributionProgress.setVisibility(View.GONE); | ||||||
|                 cancelButton.setVisibility(View.VISIBLE); |                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||||
|                 retryButton.setVisibility(View.VISIBLE); |                 binding.retryButton.setVisibility(View.VISIBLE); | ||||||
|                 pauseResumeButton.setVisibility(View.GONE); |                 binding.pauseResumeButton.setVisibility(View.GONE); | ||||||
|                 imageOptions.setVisibility(View.VISIBLE); |                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -203,11 +194,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|      */ |      */ | ||||||
|     private void displayWikipediaButton(Boolean mediaExists) { |     private void displayWikipediaButton(Boolean mediaExists) { | ||||||
|         if (!mediaExists) { |         if (!mediaExists) { | ||||||
|             addToWikipediaButton.setVisibility(View.VISIBLE); |             binding.wikipediaButton.setVisibility(View.VISIBLE); | ||||||
|             isWikipediaButtonDisplayed = true; |             isWikipediaButtonDisplayed = true; | ||||||
|             cancelButton.setVisibility(View.GONE); |             binding.cancelButton.setVisibility(View.GONE); | ||||||
|             retryButton.setVisibility(View.GONE); |             binding.retryButton.setVisibility(View.GONE); | ||||||
|             imageOptions.setVisibility(View.VISIBLE); |             binding.imageOptions.setVisibility(View.VISIBLE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -229,7 +220,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|     /** |     /** | ||||||
|      * Retry upload when it is failed |      * Retry upload when it is failed | ||||||
|      */ |      */ | ||||||
|     @OnClick(R.id.retryButton) |  | ||||||
|     public void retryUpload() { |     public void retryUpload() { | ||||||
|         callback.retryUpload(contribution); |         callback.retryUpload(contribution); | ||||||
|     } |     } | ||||||
|  | @ -237,17 +227,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|     /** |     /** | ||||||
|      * Delete a failed upload attempt |      * Delete a failed upload attempt | ||||||
|      */ |      */ | ||||||
|     @OnClick(R.id.cancelButton) |  | ||||||
|     public void deleteUpload() { |     public void deleteUpload() { | ||||||
|         callback.deleteUpload(contribution); |         callback.deleteUpload(contribution); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.contributionImage) |  | ||||||
|     public void imageClicked() { |     public void imageClicked() { | ||||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed); |         callback.openMediaDetail(position, isWikipediaButtonDisplayed); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @OnClick(R.id.wikipediaButton) |  | ||||||
|     public void wikipediaButtonClicked() { |     public void wikipediaButtonClicked() { | ||||||
|         callback.addImageToWikipedia(contribution); |         callback.addImageToWikipedia(contribution); | ||||||
|     } |     } | ||||||
|  | @ -255,9 +242,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|     /** |     /** | ||||||
|      * Triggers a callback for pause/resume |      * Triggers a callback for pause/resume | ||||||
|      */ |      */ | ||||||
|     @OnClick(R.id.pauseResumeButton) |  | ||||||
|     public void onPauseResumeButtonClicked() { |     public void onPauseResumeButtonClicked() { | ||||||
|         if (pauseResumeButton.getTag().toString().equals("pause")) { |         if (binding.pauseResumeButton.getTag().toString().equals("pause")) { | ||||||
|             pause(); |             pause(); | ||||||
|         } else { |         } else { | ||||||
|             resume(); |             resume(); | ||||||
|  | @ -279,16 +265,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|      * Update pause/resume button to show pause state |      * Update pause/resume button to show pause state | ||||||
|      */ |      */ | ||||||
|     private void setPaused() { |     private void setPaused() { | ||||||
|         pauseResumeButton.setImageResource(R.drawable.pause_icon); |         binding.pauseResumeButton.setImageResource(R.drawable.pause_icon); | ||||||
|         pauseResumeButton.setTag(parent.getContext().getString(R.string.pause)); |         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Update pause/resume button to show resume state |      * Update pause/resume button to show resume state | ||||||
|      */ |      */ | ||||||
|     private void setResume() { |     private void setResume() { | ||||||
|         pauseResumeButton.setImageResource(R.drawable.play_icon); |         binding.pauseResumeButton.setImageResource(R.drawable.play_icon); | ||||||
|         pauseResumeButton.setTag(parent.getContext().getString(R.string.resume)); |         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ImageRequest getImageRequest() { |     public ImageRequest getImageRequest() { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,21 @@ | ||||||
| package fr.free.nrw.commons.contributions; | 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_FAILED; | ||||||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; | 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.nearby.fragments.NearbyParentFragment.WLM_URL; | ||||||
| import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; | 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 static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | ||||||
| 
 | 
 | ||||||
| import android.Manifest; | import android.Manifest; | ||||||
|  | import android.Manifest.permission; | ||||||
| import android.annotation.SuppressLint; | import android.annotation.SuppressLint; | ||||||
| import android.content.Context; | 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.os.Bundle; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
|  | @ -21,6 +28,9 @@ import android.widget.CheckBox; | ||||||
| import android.widget.LinearLayout; | import android.widget.LinearLayout; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toast; | 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.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.fragment.app.Fragment; | 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.CommonsApplication; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.auth.SessionManager; | import fr.free.nrw.commons.auth.SessionManager; | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentContributionsBinding; | ||||||
| import fr.free.nrw.commons.notification.models.Notification; | import fr.free.nrw.commons.notification.models.Notification; | ||||||
| import fr.free.nrw.commons.notification.NotificationController; | import fr.free.nrw.commons.notification.NotificationController; | ||||||
| import fr.free.nrw.commons.profile.ProfileActivity; | import fr.free.nrw.commons.profile.ProfileActivity; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import androidx.work.WorkManager; | import androidx.work.WorkManager; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.campaigns.models.Campaign; | import fr.free.nrw.commons.campaigns.models.Campaign; | ||||||
|  | @ -73,18 +83,27 @@ import io.reactivex.schedulers.Schedulers; | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| public class ContributionsFragment | public class ContributionsFragment | ||||||
|         extends CommonsDaggerSupportFragment |     extends CommonsDaggerSupportFragment | ||||||
|         implements |     implements | ||||||
|         OnBackStackChangedListener, |     OnBackStackChangedListener, | ||||||
|         LocationUpdateListener, |     LocationUpdateListener, | ||||||
|     MediaDetailProvider, |     MediaDetailProvider, | ||||||
|     ICampaignsView, ContributionsContract.View, Callback{ |     SensorEventListener, | ||||||
|     @Inject @Named("default_preferences") JsonKvStore store; |     ICampaignsView, ContributionsContract.View, Callback { | ||||||
|     @Inject NearbyController nearbyController; | 
 | ||||||
|     @Inject OkHttpJsonApiClient okHttpJsonApiClient; |     @Inject | ||||||
|     @Inject CampaignsPresenter presenter; |     @Named("default_preferences") | ||||||
|     @Inject LocationServiceManager locationManager; |     JsonKvStore store; | ||||||
|     @Inject NotificationController notificationController; |     @Inject | ||||||
|  |     NearbyController nearbyController; | ||||||
|  |     @Inject | ||||||
|  |     OkHttpJsonApiClient okHttpJsonApiClient; | ||||||
|  |     @Inject | ||||||
|  |     CampaignsPresenter presenter; | ||||||
|  |     @Inject | ||||||
|  |     LocationServiceManager locationManager; | ||||||
|  |     @Inject | ||||||
|  |     NotificationController notificationController; | ||||||
| 
 | 
 | ||||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); |     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||||
| 
 | 
 | ||||||
|  | @ -92,20 +111,18 @@ public class ContributionsFragment | ||||||
|     private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; |     private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; | ||||||
|     private MediaDetailPagerFragment mediaDetailPagerFragment; |     private MediaDetailPagerFragment mediaDetailPagerFragment; | ||||||
|     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; |     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; |     public FragmentContributionsBinding binding; | ||||||
|     @BindView(R.id.limited_connection_enabled_layout) LinearLayout limitedConnectionEnabledLayout; |  | ||||||
|     @BindView(R.id.limited_connection_description_text_view) TextView limitedConnectionDescriptionTextView; |  | ||||||
| 
 | 
 | ||||||
|     @Inject ContributionsPresenter contributionsPresenter; |     @Inject ContributionsPresenter contributionsPresenter; | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
| 
 | 
 | ||||||
|     private LatLng curLatLng; |     private LatLng currentLatLng; | ||||||
| 
 | 
 | ||||||
|     private boolean firstLocationUpdate = true; |  | ||||||
|     private boolean isFragmentAttachedBefore = false; |     private boolean isFragmentAttachedBefore = false; | ||||||
|     private View checkBoxView; |     private View checkBoxView; | ||||||
|     private CheckBox checkBox; |     private CheckBox checkBox; | ||||||
|  | @ -117,6 +134,34 @@ public class ContributionsFragment | ||||||
|     String userName; |     String userName; | ||||||
|     private boolean isUserProfile; |     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 |     @NonNull | ||||||
|     public static ContributionsFragment newInstance() { |     public static ContributionsFragment newInstance() { | ||||||
|         ContributionsFragment fragment = new ContributionsFragment(); |         ContributionsFragment fragment = new ContributionsFragment(); | ||||||
|  | @ -133,17 +178,21 @@ public class ContributionsFragment | ||||||
|             userName = getArguments().getString(KEY_USERNAME); |             userName = getArguments().getString(KEY_USERNAME); | ||||||
|             isUserProfile = true; |             isUserProfile = true; | ||||||
|         } |         } | ||||||
|  |         mSensorManager = (SensorManager) getActivity().getSystemService(SENSOR_SERVICE); | ||||||
|  |         mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Nullable |     @Nullable | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | ||||||
|         View view = inflater.inflate(R.layout.fragment_contributions, container, false); |         @Nullable Bundle savedInstanceState) { | ||||||
|         ButterKnife.bind(this, view); | 
 | ||||||
|  |         binding = FragmentContributionsBinding.inflate(inflater, container, false); | ||||||
|  | 
 | ||||||
|         initWLMCampaign(); |         initWLMCampaign(); | ||||||
|         presenter.onAttachView(this); |         presenter.onAttachView(this); | ||||||
|         contributionsPresenter.onAttachView(this); |         contributionsPresenter.onAttachView(this); | ||||||
|         campaignView.setVisibility(View.GONE); |         binding.campaignsView.setVisibility(View.GONE); | ||||||
|         checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); |         checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); | ||||||
|         checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); |         checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); | ||||||
|         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { |         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||||
|  | @ -153,6 +202,7 @@ public class ContributionsFragment | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         if (savedInstanceState != null) { |         if (savedInstanceState != null) { | ||||||
|             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() |             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() | ||||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); |                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); | ||||||
|  | @ -163,13 +213,13 @@ public class ContributionsFragment | ||||||
| 
 | 
 | ||||||
|         initFragments(); |         initFragments(); | ||||||
|         if(isUserProfile) { |         if(isUserProfile) { | ||||||
|             limitedConnectionEnabledLayout.setVisibility(View.GONE); |             binding.limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||||
|         }else { |         }else { | ||||||
|             upDateUploadCount(); |             upDateUploadCount(); | ||||||
|         } |         } | ||||||
|         if(shouldShowMediaDetailsFragment){ |         if (shouldShowMediaDetailsFragment) { | ||||||
|             showMediaDetailPagerFragment(); |             showMediaDetailPagerFragment(); | ||||||
|         }else{ |         } else { | ||||||
|             if (mediaDetailPagerFragment != null) { |             if (mediaDetailPagerFragment != null) { | ||||||
|                 removeFragment(mediaDetailPagerFragment); |                 removeFragment(mediaDetailPagerFragment); | ||||||
|             } |             } | ||||||
|  | @ -180,9 +230,9 @@ public class ContributionsFragment | ||||||
|             && sessionManager.getCurrentAccount() != null && !isUserProfile) { |             && sessionManager.getCurrentAccount() != null && !isUserProfile) { | ||||||
|             setUploadCount(); |             setUploadCount(); | ||||||
|         } |         } | ||||||
|         limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); |         binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); | ||||||
|         setHasOptionsMenu(true); |         setHasOptionsMenu(true); | ||||||
|         return view; |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -195,10 +245,13 @@ public class ContributionsFragment | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @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 |         // 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); |         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"))); |                 throwable -> Timber.e(throwable, "Error occurred while loading notifications"))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void scrollToTop( ){ |     public void scrollToTop() { | ||||||
|         if (contributionsListFragment != null) { |         if (contributionsListFragment != null) { | ||||||
|             contributionsListFragment.scrollToTop(); |             contributionsListFragment.scrollToTop(); | ||||||
|         } |         } | ||||||
|  | @ -242,22 +295,17 @@ public class ContributionsFragment | ||||||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); |             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||||
| 
 | 
 | ||||||
|         checkable.setChecked(isEnabled); |         checkable.setChecked(isEnabled); | ||||||
|         if (isEnabled) { |         if (binding!=null) { | ||||||
|             limitedConnectionEnabledLayout.setVisibility(View.VISIBLE); |             binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||||
|         } else { |  | ||||||
|             limitedConnectionEnabledLayout.setVisibility(View.GONE); |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); |         checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||||
|         checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() { |         checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() { | ||||||
|             @Override |             @Override | ||||||
|             public boolean onMenuItemClick(MenuItem item) { |             public boolean onMenuItemClick(MenuItem item) { | ||||||
|                 ((MainActivity) getActivity()).toggleLimitedConnectionMode(); |                 ((MainActivity) getActivity()).toggleLimitedConnectionMode(); | ||||||
|                 boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); |                 boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||||
|                 if (isEnabled) { |                 binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||||
|                     limitedConnectionEnabledLayout.setVisibility(View.VISIBLE); |  | ||||||
|                 } else { |  | ||||||
|                     limitedConnectionEnabledLayout.setVisibility(View.GONE); |  | ||||||
|                 } |  | ||||||
|                 checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); |                 checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|  | @ -285,28 +333,31 @@ public class ContributionsFragment | ||||||
|      */ |      */ | ||||||
|     private void showContributionsListFragment() { |     private void showContributionsListFragment() { | ||||||
|         // show nearby card view on contributions list is visible |         // show nearby card view on contributions list is visible | ||||||
|         if (nearbyNotificationCardView != null && !isUserProfile) { |         if (binding.cardViewNearby != null && !isUserProfile) { | ||||||
|             if (store.getBoolean("displayNearbyCardView", true)) { |             if (store.getBoolean("displayNearbyCardView", true)) { | ||||||
|                 if (nearbyNotificationCardView.cardViewVisibilityState |                 if (binding.cardViewNearby.cardViewVisibilityState | ||||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { |                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||||
|                     nearbyNotificationCardView.setVisibility(View.VISIBLE); |                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||||
|                 } |                 } | ||||||
|             } else { |             } 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() { |     private void showMediaDetailPagerFragment() { | ||||||
|         // hide nearby card view on media detail is visible |         // hide nearby card view on media detail is visible | ||||||
|         setupViewForMediaDetails(); |         setupViewForMediaDetails(); | ||||||
|         showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, contributionsListFragment); |         showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, | ||||||
|  |             contributionsListFragment); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupViewForMediaDetails() { |     private void setupViewForMediaDetails() { | ||||||
|         campaignView.setVisibility(View.GONE); |         if (binding!=null) { | ||||||
|         nearbyNotificationCardView.setVisibility(View.GONE); |             binding.campaignsView.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -328,7 +379,8 @@ public class ContributionsFragment | ||||||
|             showContributionsListFragment(); |             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.addToBackStack(tag); | ||||||
|             transaction.commit(); |             transaction.commit(); | ||||||
|             getChildFragmentManager().executePendingTransactions(); |             getChildFragmentManager().executePendingTransactions(); | ||||||
|         }else if (!fragment.isAdded() && otherFragment != null ) { |         } else if (!fragment.isAdded() && otherFragment != null) { | ||||||
|             transaction.hide(otherFragment); |             transaction.hide(otherFragment); | ||||||
|             transaction.add(R.id.root_frame, fragment, tag); |             transaction.add(R.id.root_frame, fragment, tag); | ||||||
|             transaction.addToBackStack(tag); |             transaction.addToBackStack(tag); | ||||||
|  | @ -376,21 +428,21 @@ public class ContributionsFragment | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|     private void setUploadCount() { |     private void setUploadCount() { | ||||||
|         compositeDisposable.add(okHttpJsonApiClient |         compositeDisposable.add(okHttpJsonApiClient | ||||||
|                 .getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name) |             .getUploadCount(((MainActivity) getActivity()).sessionManager.getCurrentAccount().name) | ||||||
|                 .subscribeOn(Schedulers.io()) |             .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |             .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(this::displayUploadCount, |             .subscribe(this::displayUploadCount, | ||||||
|                         t -> Timber.e(t, "Fetching upload count failed") |                 t -> Timber.e(t, "Fetching upload count failed") | ||||||
|                 )); |             )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void displayUploadCount(Integer uploadCount) { |     private void displayUploadCount(Integer uploadCount) { | ||||||
|         if (getActivity().isFinishing() |         if (getActivity().isFinishing() | ||||||
|                 || getResources() == null) { |             || getResources() == null) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         ((MainActivity)getActivity()).setNumOfUploads(uploadCount); |         ((MainActivity) getActivity()).setNumOfUploads(uploadCount); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -399,6 +451,7 @@ public class ContributionsFragment | ||||||
|         super.onPause(); |         super.onPause(); | ||||||
|         locationManager.removeLocationListener(this); |         locationManager.removeLocationListener(this); | ||||||
|         locationManager.unregisterLocationManager(); |         locationManager.unregisterLocationManager(); | ||||||
|  |         mSensorManager.unregisterListener(this); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -410,9 +463,13 @@ public class ContributionsFragment | ||||||
|     public void onResume() { |     public void onResume() { | ||||||
|         super.onResume(); |         super.onResume(); | ||||||
|         contributionsPresenter.onAttachView(this); |         contributionsPresenter.onAttachView(this); | ||||||
|         firstLocationUpdate = true; |  | ||||||
|         locationManager.addLocationListener(this); |         locationManager.addLocationListener(this); | ||||||
|         nearbyNotificationCardView.permissionRequestButton.setOnClickListener(v -> { | 
 | ||||||
|  |         if (binding==null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         binding.cardViewNearby.permissionRequestButton.setOnClickListener(v -> { | ||||||
|             showNearbyCardPermissionRationale(); |             showNearbyCardPermissionRationale(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -420,13 +477,20 @@ public class ContributionsFragment | ||||||
|         if (mediaDetailPagerFragment == null && !isUserProfile) { |         if (mediaDetailPagerFragment == null && !isUserProfile) { | ||||||
|             if (store.getBoolean("displayNearbyCardView", true)) { |             if (store.getBoolean("displayNearbyCardView", true)) { | ||||||
|                 checkPermissionsAndShowNearbyCardView(); |                 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 { |             } else { | ||||||
|                 // Hide nearby notification card view if related shared preferences is false |                 // 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 |             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||||
|  | @ -435,83 +499,97 @@ public class ContributionsFragment | ||||||
|                 fetchCampaigns(); |                 fetchCampaigns(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void checkPermissionsAndShowNearbyCardView() { |     private void checkPermissionsAndShowNearbyCardView() { | ||||||
|         if (PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)) { |         if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||||
|             onLocationPermissionGranted(); |             onLocationPermissionGranted(); | ||||||
|         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) |         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||||
|                 && store.getBoolean("displayLocationPermissionForCardView", true) |             && store.getBoolean("displayLocationPermissionForCardView", true) | ||||||
|                 && !store.getBoolean("doNotAskForLocationPermission", false) |             && !store.getBoolean("doNotAskForLocationPermission", false) | ||||||
|                 && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { |             && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||||
|             nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; |             binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||||
|             showNearbyCardPermissionRationale(); |             showNearbyCardPermissionRationale(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void requestLocationPermission() { |     private void requestLocationPermission() { | ||||||
|         PermissionUtils.checkPermissionsAndPerformAction(getActivity(), |         nearbyLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION}); | ||||||
|                 Manifest.permission.ACCESS_FINE_LOCATION, |  | ||||||
|                 this::onLocationPermissionGranted, |  | ||||||
|                 this::displayYouWontSeeNearbyMessage, |  | ||||||
|                 -1, |  | ||||||
|                 -1); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void onLocationPermissionGranted() { |     private void onLocationPermissionGranted() { | ||||||
|         nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; |         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; | ||||||
|         locationManager.registerLocationManager(); |         locationManager.registerLocationManager(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void showNearbyCardPermissionRationale() { |     private void showNearbyCardPermissionRationale() { | ||||||
|         DialogUtil.showAlertDialog(getActivity(), |         DialogUtil.showAlertDialog(getActivity(), | ||||||
|                 getString(R.string.nearby_card_permission_title), |             getString(R.string.nearby_card_permission_title), | ||||||
|                 getString(R.string.nearby_card_permission_explanation), |             getString(R.string.nearby_card_permission_explanation), | ||||||
|                 this::requestLocationPermission, |             this::requestLocationPermission, | ||||||
|                 this::displayYouWontSeeNearbyMessage, |             this::displayYouWontSeeNearbyMessage, | ||||||
|                 checkBoxView, |             checkBoxView, | ||||||
|                 false); |             false); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void displayYouWontSeeNearbyMessage() { |     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); |         store.putBoolean("doNotAskForLocationPermission", true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private void updateClosestNearbyCardViewInfo() { |     private void updateClosestNearbyCardViewInfo() { | ||||||
|         curLatLng = locationManager.getLastLocation(); |         currentLatLng = locationManager.getLastLocation(); | ||||||
|         compositeDisposable.add(Observable.fromCallable(() -> nearbyController |         compositeDisposable.add(Observable.fromCallable(() -> nearbyController | ||||||
|                 .loadAttractionsFromLocation(curLatLng, curLatLng, true, false, false)) // thanks to boolean, it will only return closest result |                 .loadAttractionsFromLocation(currentLatLng, currentLatLng, true, | ||||||
|                 .subscribeOn(Schedulers.io()) |                     false)) // thanks to boolean, it will only return closest result | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |             .subscribeOn(Schedulers.io()) | ||||||
|                 .subscribe(this::updateNearbyNotification, |             .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                         throwable -> { |             .subscribe(this::updateNearbyNotification, | ||||||
|                             Timber.d(throwable); |                 throwable -> { | ||||||
|                             updateNearbyNotification(null); |                     Timber.d(throwable); | ||||||
|                         })); |                     updateNearbyNotification(null); | ||||||
|  |                 })); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { |     private void updateNearbyNotification( | ||||||
|         if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) { |         @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { | ||||||
|             Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0); |         if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null | ||||||
|             String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location); |             && nearbyPlacesInfo.placeList.size() > 0) { | ||||||
|             closestNearbyPlace.setDistance(distance); |             Place closestNearbyPlace = null; | ||||||
|             nearbyNotificationCardView.updateContent(closestNearbyPlace); |             // 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 { |         } else { | ||||||
|             // Means that no close nearby place is found |             // 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 |         // 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()) { |         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||||
|             nearbyNotificationCardView.setVisibility(View.GONE); |             binding.cardViewNearby.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onDestroy() { |     public void onDestroy() { | ||||||
|         try{ |         try { | ||||||
|             compositeDisposable.clear(); |             compositeDisposable.clear(); | ||||||
|             getChildFragmentManager().removeOnBackStackChangedListener(this); |             getChildFragmentManager().removeOnBackStackChangedListener(this); | ||||||
|             locationManager.unregisterLocationManager(); |             locationManager.unregisterLocationManager(); | ||||||
|  | @ -525,22 +603,17 @@ public class ContributionsFragment | ||||||
|     @Override |     @Override | ||||||
|     public void onLocationChangedSignificantly(LatLng latLng) { |     public void onLocationChangedSignificantly(LatLng latLng) { | ||||||
|         // Will be called if location changed more than 1000 meter |         // Will be called if location changed more than 1000 meter | ||||||
|         // Do nothing on slight changes for using network efficiently |  | ||||||
|         firstLocationUpdate = false; |  | ||||||
|         updateClosestNearbyCardViewInfo(); |         updateClosestNearbyCardViewInfo(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onLocationChangedSlightly(LatLng latLng) { |     public void onLocationChangedSlightly(LatLng latLng) { | ||||||
|         /* Update closest nearby notification card onLocationChangedSlightly |         /* 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 |         try { | ||||||
|         */ |  | ||||||
|         if (firstLocationUpdate) { |  | ||||||
|             updateClosestNearbyCardViewInfo(); |             updateClosestNearbyCardViewInfo(); | ||||||
|             // Turn it to false, since it is not first location update anymore. To change closest location |         } catch (Exception e) { | ||||||
|             // notification, we need to wait for a significant location change. |             Timber.e(e); | ||||||
|             firstLocationUpdate = false; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -550,7 +623,8 @@ public class ContributionsFragment | ||||||
|         updateClosestNearbyCardViewInfo(); |         updateClosestNearbyCardViewInfo(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override public void onViewCreated(@NonNull View view, |     @Override | ||||||
|  |     public void onViewCreated(@NonNull View view, | ||||||
|         @Nullable Bundle savedInstanceState) { |         @Nullable Bundle savedInstanceState) { | ||||||
|         super.onViewCreated(view, savedInstanceState); |         super.onViewCreated(view, savedInstanceState); | ||||||
|     } |     } | ||||||
|  | @ -562,26 +636,35 @@ public class ContributionsFragment | ||||||
|      */ |      */ | ||||||
|     private void fetchCampaigns() { |     private void fetchCampaigns() { | ||||||
|         if (Utils.isMonumentsEnabled(new Date())) { |         if (Utils.isMonumentsEnabled(new Date())) { | ||||||
|             campaignView.setCampaign(wlmCampaign); |             if (binding!=null) { | ||||||
|             campaignView.setVisibility(View.VISIBLE); |                 binding.campaignsView.setCampaign(wlmCampaign); | ||||||
|  |                 binding.campaignsView.setVisibility(View.VISIBLE); | ||||||
|  |             } | ||||||
|         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { |         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||||
|             presenter.getCampaigns(); |             presenter.getCampaigns(); | ||||||
|         } else { |         } 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(); |         Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override public void showCampaigns(Campaign campaign) { |     @Override | ||||||
|  |     public void showCampaigns(Campaign campaign) { | ||||||
|         if (campaign != null && !isUserProfile) { |         if (campaign != null && !isUserProfile) { | ||||||
|             campaignView.setCampaign(campaign); |             if (binding!=null) { | ||||||
|  |                 binding.campaignsView.setCampaign(campaign); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override public void onDestroyView() { |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|         super.onDestroyView(); |         super.onDestroyView(); | ||||||
|         presenter.onDetachView(); |         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 |      * Retry upload when it is failed | ||||||
|      * |      * | ||||||
|  | @ -601,10 +695,25 @@ public class ContributionsFragment | ||||||
|     @Override |     @Override | ||||||
|     public void retryUpload(Contribution contribution) { |     public void retryUpload(Contribution contribution) { | ||||||
|         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { |         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { | ||||||
|             if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { |             if (contribution.getState() == STATE_PAUSED | ||||||
|                 contribution.setState(Contribution.STATE_QUEUED); |                 || contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { | ||||||
|                 contributionsPresenter.saveContribution(contribution); |                 restartUpload(contribution); | ||||||
|                 Timber.d("Restarting for %s", contribution.toString()); |             } 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 { |             } else { | ||||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); |                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); | ||||||
|             } |             } | ||||||
|  | @ -616,6 +725,7 @@ public class ContributionsFragment | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Pauses the upload |      * Pauses the upload | ||||||
|  |      * | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|  | @ -639,15 +749,15 @@ public class ContributionsFragment | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Replace whatever is in the current contributionsFragmentContainer view with |      * Replace whatever is in the current contributionsFragmentContainer view with | ||||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a |      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||||
|      * contribution. |      * a contribution. | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void showDetail(int position, boolean isWikipediaButtonDisplayed) { |     public void showDetail(int position, boolean isWikipediaButtonDisplayed) { | ||||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { |         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { | ||||||
|             mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); |             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|             if(isUserProfile) { |             if (isUserProfile) { | ||||||
|                 ((ProfileActivity)getActivity()).setScroll(false); |                 ((ProfileActivity) getActivity()).setScroll(false); | ||||||
|             } |             } | ||||||
|             showMediaDetailPagerFragment(); |             showMediaDetailPagerFragment(); | ||||||
|         } |         } | ||||||
|  | @ -672,24 +782,26 @@ public class ContributionsFragment | ||||||
|     public boolean backButtonClicked() { |     public boolean backButtonClicked() { | ||||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { |         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { | ||||||
|             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { |             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { | ||||||
|                 if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { |                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||||
|                     nearbyNotificationCardView.setVisibility(View.VISIBLE); |                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 nearbyNotificationCardView.setVisibility(View.GONE); |                 binding.cardViewNearby.setVisibility(View.GONE); | ||||||
|             } |             } | ||||||
|             removeFragment(mediaDetailPagerFragment); |             removeFragment(mediaDetailPagerFragment); | ||||||
|             showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment); |             showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||||
|             if(isUserProfile) { |                 mediaDetailPagerFragment); | ||||||
|  |             if (isUserProfile) { | ||||||
|                 // Fragment is associated with ProfileActivity |                 // Fragment is associated with ProfileActivity | ||||||
|                 // Enable ParentViewPager Scroll |                 // Enable ParentViewPager Scroll | ||||||
|                 ((ProfileActivity)getActivity()).setScroll(true); |                 ((ProfileActivity) getActivity()).setScroll(true); | ||||||
|             }else { |             } else { | ||||||
|                 fetchCampaigns(); |                 fetchCampaigns(); | ||||||
|             } |             } | ||||||
|             if (getActivity() instanceof MainActivity) { |             if (getActivity() instanceof MainActivity) { | ||||||
|                 // Fragment is associated with MainActivity |                 // Fragment is associated with MainActivity | ||||||
|                 ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); |                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||||
|  |                     .setDisplayHomeAsUpEnabled(false); | ||||||
|                 ((MainActivity) getActivity()).showTabs(); |                 ((MainActivity) getActivity()).showTabs(); | ||||||
|             } |             } | ||||||
|             return true; |             return true; | ||||||
|  | @ -709,11 +821,11 @@ public class ContributionsFragment | ||||||
|     void upDateUploadCount() { |     void upDateUploadCount() { | ||||||
|         WorkManager.getInstance(getContext()) |         WorkManager.getInstance(getContext()) | ||||||
|             .getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe( |             .getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe( | ||||||
|             getViewLifecycleOwner(), workInfos -> { |                 getViewLifecycleOwner(), workInfos -> { | ||||||
|                 if (workInfos.size() > 0) { |                     if (workInfos.size() > 0) { | ||||||
|                     setUploadCount(); |                         setUploadCount(); | ||||||
|                 } |                     } | ||||||
|             }); |                 }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -724,29 +836,40 @@ public class ContributionsFragment | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void refreshNominatedMedia(int index) { |     public void refreshNominatedMedia(int index) { | ||||||
|         if(mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { |         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||||
|             removeFragment(mediaDetailPagerFragment); |             removeFragment(mediaDetailPagerFragment); | ||||||
|             mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); |             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|             mediaDetailPagerFragment.showImage(index); |             mediaDetailPagerFragment.showImage(index); | ||||||
|             showMediaDetailPagerFragment(); |             showMediaDetailPagerFragment(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   // click listener to toggle description that means uses can press the limited connection |     // click listener to toggle description that means uses can press the limited connection | ||||||
|   // banner and description will hide. Tap again to show description. |     // banner and description will hide. Tap again to show description. | ||||||
|   private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { |     private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { | ||||||
| 
 | 
 | ||||||
|       @Override |         @Override | ||||||
|       public void onClick(View view) { |         public void onClick(View view) { | ||||||
|           View view2 = limitedConnectionDescriptionTextView; |             View view2 = binding.limitedConnectionDescriptionTextView; | ||||||
|           if (view2.getVisibility() == View.GONE) { |             if (view2.getVisibility() == View.GONE) { | ||||||
|               view2.setVisibility(View.VISIBLE); |                 view2.setVisibility(View.VISIBLE); | ||||||
|           } else { |             } else { | ||||||
|               view2.setVisibility(View.GONE); |                 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; | package fr.free.nrw.commons.contributions; | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.BasePresenter; | import fr.free.nrw.commons.BasePresenter; | ||||||
| import java.util.List; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The contract for Contributions list View & Presenter |  * 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 android.view.View.VISIBLE; | ||||||
| import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE; | 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.Context; | ||||||
| import android.content.res.Configuration; | import android.content.res.Configuration; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
|  | @ -16,11 +17,12 @@ import android.view.ViewGroup; | ||||||
| import android.view.animation.Animation; | import android.view.animation.Animation; | ||||||
| import android.view.animation.AnimationUtils; | import android.view.animation.AnimationUtils; | ||||||
| import android.widget.LinearLayout; | import android.widget.LinearLayout; | ||||||
| import android.widget.ProgressBar; | import androidx.activity.result.ActivityResultCallback; | ||||||
| import android.widget.TextView; | import androidx.activity.result.ActivityResultLauncher; | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts; | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.widget.AppCompatTextView; | import androidx.annotation.VisibleForTesting; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import androidx.recyclerview.widget.GridLayoutManager; | import androidx.recyclerview.widget.GridLayoutManager; | ||||||
| import androidx.recyclerview.widget.RecyclerView; | 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.ItemAnimator; | ||||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | ||||||
| import androidx.recyclerview.widget.SimpleItemAnimator; | import androidx.recyclerview.widget.SimpleItemAnimator; | ||||||
| import butterknife.BindView; | import fr.free.nrw.commons.CommonsApplication; | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import butterknife.OnClick; |  | ||||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.auth.SessionManager; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.utils.DialogUtil; |  | ||||||
| import fr.free.nrw.commons.media.MediaClient; | import fr.free.nrw.commons.media.MediaClient; | ||||||
|  | import fr.free.nrw.commons.profile.ProfileActivity; | ||||||
|  | import fr.free.nrw.commons.utils.DialogUtil; | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  | import java.util.Map; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.wikipedia.dataclient.WikiSite; | import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||||
| import fr.free.nrw.commons.profile.ProfileActivity; |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -60,63 +61,72 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
| 
 | 
 | ||||||
|     private static final String RV_STATE = "rv_scroll_state"; |     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 |     @Inject | ||||||
|     SystemThemeUtils systemThemeUtils; |     SystemThemeUtils systemThemeUtils; | ||||||
|     @BindView(R.id.tv_contributions_of_user) |  | ||||||
|     AppCompatTextView tvContributionsOfUser; |  | ||||||
| 
 |  | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionController controller; |     ContributionController controller; | ||||||
|     @Inject |     @Inject | ||||||
|     MediaClient mediaClient; |     MediaClient mediaClient; | ||||||
| 
 |  | ||||||
|     @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) |     @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) | ||||||
|     @Inject |     @Inject | ||||||
|     WikiSite languageWikipediaSite; |     WikiSite languageWikipediaSite; | ||||||
| 
 |  | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionsListPresenter contributionsListPresenter; |     ContributionsListPresenter contributionsListPresenter; | ||||||
| 
 |  | ||||||
|     @Inject |     @Inject | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
| 
 | 
 | ||||||
|  |     private FragmentContributionsListBinding binding; | ||||||
|     private Animation fab_close; |     private Animation fab_close; | ||||||
|     private Animation fab_open; |     private Animation fab_open; | ||||||
|     private Animation rotate_forward; |     private Animation rotate_forward; | ||||||
|     private Animation rotate_backward; |     private Animation rotate_backward; | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private boolean isFabOpen; |     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_LANDSCAPE = 3; | ||||||
|     private final int SPAN_COUNT_PORTRAIT = 1; |     private final int SPAN_COUNT_PORTRAIT = 1; | ||||||
| 
 | 
 | ||||||
|     private int contributionsSize; |     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 |     @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); |         super.onCreate(savedInstanceState); | ||||||
|         //Now that we are allowing this fragment to be started for |         //Now that we are allowing this fragment to be started for | ||||||
|         // any userName- we expect it to be passed as an argument |         // any userName- we expect it to be passed as an argument | ||||||
|  | @ -133,21 +143,36 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     public View onCreateView( |     public View onCreateView( | ||||||
|         final LayoutInflater inflater, @Nullable final ViewGroup container, |         final LayoutInflater inflater, @Nullable final ViewGroup container, | ||||||
|         @Nullable final Bundle savedInstanceState) { |         @Nullable final Bundle savedInstanceState) { | ||||||
|         final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); |         binding = FragmentContributionsListBinding.inflate( | ||||||
|         ButterKnife.bind(this, view); |             inflater, container, false | ||||||
|  |         ); | ||||||
|  |         rvContributionsList = binding.contributionsList; | ||||||
|  | 
 | ||||||
|         contributionsListPresenter.onAttachView(this); |         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)) { |         if (Objects.equals(sessionManager.getUserName(), userName)) { | ||||||
|             tvContributionsOfUser.setVisibility(GONE); |             binding.tvContributionsOfUser.setVisibility(GONE); | ||||||
|             fab_layout.setVisibility(VISIBLE); |             binding.fabLayout.setVisibility(VISIBLE); | ||||||
|         } else { |         } else { | ||||||
|             tvContributionsOfUser.setVisibility(VISIBLE); |             binding.tvContributionsOfUser.setVisibility(VISIBLE); | ||||||
|             tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName)); |             binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName)); | ||||||
|             fab_layout.setVisibility(GONE); |             binding.fabLayout.setVisibility(GONE); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         initAdapter(); |         initAdapter(); | ||||||
|         return view; | 
 | ||||||
|  |         return binding.getRoot(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|  |         binding = null; | ||||||
|  |         super.onDestroyView(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -280,7 +305,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     public void onConfigurationChanged(final Configuration newConfig) { |     public void onConfigurationChanged(final Configuration newConfig) { | ||||||
|         super.onConfigurationChanged(newConfig); |         super.onConfigurationChanged(newConfig); | ||||||
|         // check orientation |         // check orientation | ||||||
|         fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? |         binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||||
|             LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); |             LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); | ||||||
|         rvContributionsList |         rvContributionsList | ||||||
|             .setLayoutManager( |             .setLayoutManager( | ||||||
|  | @ -295,22 +320,29 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setListeners() { |     private void setListeners() { | ||||||
|         fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); |         binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); | ||||||
|         fabCamera.setOnClickListener(view -> { |         binding.fabCamera.setOnClickListener(view -> { | ||||||
|             controller.initiateCameraPick(getActivity()); |             controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher); | ||||||
|             animateFAB(isFabOpen); |             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); |             controller.initiateGalleryPick(getActivity(), true); | ||||||
|             animateFAB(isFabOpen); |             animateFAB(isFabOpen); | ||||||
|         }); |         }); | ||||||
|  |         binding.fabGallery.setOnLongClickListener(view -> { | ||||||
|  |             ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery); | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Launch Custom Selector. |      * Launch Custom Selector. | ||||||
|      */ |      */ | ||||||
|     @OnClick(R.id.fab_custom_gallery) |     protected void launchCustomSelector() { | ||||||
|     void launchCustomSelector(){ |  | ||||||
|         controller.initiateCustomGalleryPickWithPermission(getActivity()); |         controller.initiateCustomGalleryPickWithPermission(getActivity()); | ||||||
|         animateFAB(isFabOpen); |         animateFAB(isFabOpen); | ||||||
|     } |     } | ||||||
|  | @ -321,25 +353,25 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
| 
 | 
 | ||||||
|     private void animateFAB(final boolean isFabOpen) { |     private void animateFAB(final boolean isFabOpen) { | ||||||
|         this.isFabOpen = !isFabOpen; |         this.isFabOpen = !isFabOpen; | ||||||
|         if (fabPlus.isShown()) { |         if (binding.fabPlus.isShown()) { | ||||||
|         if (isFabOpen) { |             if (isFabOpen) { | ||||||
|             fabPlus.startAnimation(rotate_backward); |                 binding.fabPlus.startAnimation(rotate_backward); | ||||||
|             fabCamera.startAnimation(fab_close); |                 binding.fabCamera.startAnimation(fab_close); | ||||||
|             fabGallery.startAnimation(fab_close); |                 binding.fabGallery.startAnimation(fab_close); | ||||||
|             fabCustomGallery.startAnimation(fab_close); |                 binding.fabCustomGallery.startAnimation(fab_close); | ||||||
|             fabCamera.hide(); |                 binding.fabCamera.hide(); | ||||||
|             fabGallery.hide(); |                 binding.fabGallery.hide(); | ||||||
|             fabCustomGallery.hide(); |                 binding.fabCustomGallery.hide(); | ||||||
|         } else { |             } else { | ||||||
|             fabPlus.startAnimation(rotate_forward); |                 binding.fabPlus.startAnimation(rotate_forward); | ||||||
|             fabCamera.startAnimation(fab_open); |                 binding.fabCamera.startAnimation(fab_open); | ||||||
|             fabGallery.startAnimation(fab_open); |                 binding.fabGallery.startAnimation(fab_open); | ||||||
|             fabCustomGallery.startAnimation(fab_open); |                 binding.fabCustomGallery.startAnimation(fab_open); | ||||||
|             fabCamera.show(); |                 binding.fabCamera.show(); | ||||||
|             fabGallery.show(); |                 binding.fabGallery.show(); | ||||||
|             fabCustomGallery.show(); |                 binding.fabCustomGallery.show(); | ||||||
|         } |             } | ||||||
|         this.isFabOpen = !isFabOpen; |             this.isFabOpen = !isFabOpen; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -348,7 +380,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void showWelcomeTip(final boolean shouldShow) { |     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 |     @Override | ||||||
|     public void showProgress(final boolean shouldShow) { |     public void showProgress(final boolean shouldShow) { | ||||||
|         progressBar.setVisibility(shouldShow ? VISIBLE : GONE); |         binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void showNoContributionsUI(final boolean shouldShow) { |     public void showNoContributionsUI(final boolean shouldShow) { | ||||||
|         noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); |         binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -393,14 +425,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     @Override |     @Override | ||||||
|     public void deleteUpload(final Contribution contribution) { |     public void deleteUpload(final Contribution contribution) { | ||||||
|         DialogUtil.showAlertDialog(getActivity(), |         DialogUtil.showAlertDialog(getActivity(), | ||||||
|             String.format(getString(R.string.cancelling_upload), |             String.format(Locale.getDefault(), | ||||||
|                 Locale.getDefault().getDisplayLanguage()), |                 getString(R.string.cancelling_upload)), | ||||||
|             String.format(getString(R.string.cancel_upload_dialog), |             String.format(Locale.getDefault(), | ||||||
|                 Locale.getDefault().getDisplayLanguage()), |                 getString(R.string.cancel_upload_dialog)), | ||||||
|             "YES", "NO", |             String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)), | ||||||
|             () -> { |             () -> { | ||||||
|                 ViewUtil.showShortToast(getContext(), R.string.cancelling_upload); |                 ViewUtil.showShortToast(getContext(), R.string.cancelling_upload); | ||||||
|                 contributionsListPresenter.deleteUpload(contribution); |                 contributionsListPresenter.deleteUpload(contribution); | ||||||
|  |                 CommonsApplication.cancelledUploads.add(contribution.getPageId()); | ||||||
|             }, () -> { |             }, () -> { | ||||||
|                 // Do nothing |                 // Do nothing | ||||||
|             }); |             }); | ||||||
|  | @ -422,8 +455,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     public void addImageToWikipedia(Contribution contribution) { |     public void addImageToWikipedia(Contribution contribution) { | ||||||
|         DialogUtil.showAlertDialog(getActivity(), |         DialogUtil.showAlertDialog(getActivity(), | ||||||
|             getString(R.string.add_picture_to_wikipedia_article_title), |             getString(R.string.add_picture_to_wikipedia_article_title), | ||||||
|             String.format(getString(R.string.add_picture_to_wikipedia_article_desc), |             getString(R.string.add_picture_to_wikipedia_article_desc), | ||||||
|                 Locale.getDefault().getDisplayLanguage()), |  | ||||||
|             () -> { |             () -> { | ||||||
|                 showAddImageToWikipediaInstructions(contribution); |                 showAddImageToWikipediaInstructions(contribution); | ||||||
|             }, () -> { |             }, () -> { | ||||||
|  |  | ||||||
|  | @ -1,18 +1,12 @@ | ||||||
| package fr.free.nrw.commons.contributions; | package fr.free.nrw.commons.contributions; | ||||||
| 
 | 
 | ||||||
| import androidx.work.ExistingWorkPolicy; | import androidx.work.ExistingWorkPolicy; | ||||||
| import androidx.work.OneTimeWorkRequest; |  | ||||||
| import androidx.work.WorkManager; |  | ||||||
| import fr.free.nrw.commons.MediaDataExtractor; | import fr.free.nrw.commons.MediaDataExtractor; | ||||||
| import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | ||||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | 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.Scheduler; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | 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.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| 
 | 
 | ||||||
|  | @ -76,11 +70,7 @@ public class ContributionsPresenter implements UserActionListener { | ||||||
|         compositeDisposable.add(repository |         compositeDisposable.add(repository | ||||||
|             .save(contribution) |             .save(contribution) | ||||||
|             .subscribeOn(ioThreadScheduler) |             .subscribeOn(ioThreadScheduler) | ||||||
|             .subscribe(() -> { |             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||||
|                 WorkManager.getInstance(view.getContext().getApplicationContext()) |                 view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP))); | ||||||
|                     .enqueueUniqueWork( |  | ||||||
|                         UploadWorker.class.getSimpleName(), |  | ||||||
|                         ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); |  | ||||||
|             })); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,28 +1,24 @@ | ||||||
| package fr.free.nrw.commons.contributions; | package fr.free.nrw.commons.contributions; | ||||||
| 
 | 
 | ||||||
| import android.Manifest.permission; | import android.Manifest.permission; | ||||||
|  | import android.annotation.SuppressLint; | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.content.pm.PackageManager; |  | ||||||
| import android.os.Build.VERSION; | import android.os.Build.VERSION; | ||||||
| import android.os.Build.VERSION_CODES; | import android.os.Build.VERSION_CODES; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.FrameLayout; |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.widget.Toolbar; |  | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
|  | import androidx.viewpager.widget.ViewPager; | ||||||
| import androidx.work.ExistingWorkPolicy; | import androidx.work.ExistingWorkPolicy; | ||||||
| import androidx.work.OneTimeWorkRequest; | import fr.free.nrw.commons.databinding.MainBinding; | ||||||
| import androidx.work.WorkManager; |  | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.CommonsApplication; | import fr.free.nrw.commons.CommonsApplication; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.WelcomeActivity; | 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.quiz.QuizChecker; | ||||||
| import fr.free.nrw.commons.settings.SettingsFragment; | import fr.free.nrw.commons.settings.SettingsFragment; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | 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.PermissionUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | 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.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
|  | @ -59,14 +59,8 @@ public class MainActivity  extends BaseActivity | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionController controller; |     ContributionController controller; | ||||||
|     @BindView(R.id.toolbar) |     @Inject | ||||||
|     Toolbar toolbar; |     ContributionDao contributionDao; | ||||||
|     @BindView(R.id.pager) |  | ||||||
|     public UnswipableViewPager viewPager; |  | ||||||
|     @BindView(R.id.fragmentContainer) |  | ||||||
|     public FrameLayout fragmentContainer; |  | ||||||
|     @BindView(R.id.fragment_main_nav_tab_layout) |  | ||||||
|     NavTabLayout tabLayout; |  | ||||||
| 
 | 
 | ||||||
|     private ContributionsFragment contributionsFragment; |     private ContributionsFragment contributionsFragment; | ||||||
|     private NearbyParentFragment nearbyParentFragment; |     private NearbyParentFragment nearbyParentFragment; | ||||||
|  | @ -91,6 +85,11 @@ public class MainActivity  extends BaseActivity | ||||||
| 
 | 
 | ||||||
|     public Menu menu; |     public Menu menu; | ||||||
| 
 | 
 | ||||||
|  |     public MainBinding binding; | ||||||
|  | 
 | ||||||
|  |     NavTabLayout tabLayout; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Consumers should be simply using this method to use this activity. |      * Consumers should be simply using this method to use this activity. | ||||||
|      * |      * | ||||||
|  | @ -118,11 +117,13 @@ public class MainActivity  extends BaseActivity | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(Bundle savedInstanceState) { |     public void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|  |         binding = MainBinding.inflate(getLayoutInflater()); | ||||||
|  |         setContentView(binding.getRoot()); | ||||||
|  |         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||||
|  |         tabLayout = binding.fragmentMainNavTabLayout; | ||||||
|         loadLocale(); |         loadLocale(); | ||||||
|         setContentView(R.layout.main); | 
 | ||||||
|         ButterKnife.bind(this); |         binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|         toolbar.setNavigationOnClickListener(view -> { |  | ||||||
|             onSupportNavigateUp(); |             onSupportNavigateUp(); | ||||||
|         }); |         }); | ||||||
|         /* |         /* | ||||||
|  | @ -139,6 +140,10 @@ public class MainActivity  extends BaseActivity | ||||||
|             setTitle(getString(R.string.navigation_item_explore)); |             setTitle(getString(R.string.navigation_item_explore)); | ||||||
|             setUpLoggedOutPager(); |             setUpLoggedOutPager(); | ||||||
|         } else { |         } else { | ||||||
|  |             if (applicationKvStore.getBoolean("firstrun", true)) { | ||||||
|  |                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); | ||||||
|  |                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); | ||||||
|  |             } | ||||||
|             if(savedInstanceState == null){ |             if(savedInstanceState == null){ | ||||||
|                 //starting a fresh fragment. |                 //starting a fresh fragment. | ||||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions |                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions | ||||||
|  | @ -152,15 +157,29 @@ public class MainActivity  extends BaseActivity | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             setUpPager(); |             setUpPager(); | ||||||
|  |             /** | ||||||
|  |              * Ask the user for media location access just after login | ||||||
|  |              * so that location in the EXIF metadata of the images shared by the user | ||||||
|  |              * is retained on devices running Android 10 or above | ||||||
|  |              */ | ||||||
|  |             if (VERSION.SDK_INT >= VERSION_CODES.Q) { | ||||||
|  |                 PermissionUtils.checkPermissionsAndPerformAction( | ||||||
|  |                     this, | ||||||
|  |                     () -> {}, | ||||||
|  |                     R.string.media_location_permission_denied, | ||||||
|  |                     R.string.add_location_manually, | ||||||
|  |                     permission.ACCESS_MEDIA_LOCATION); | ||||||
|  |             } | ||||||
|  |             checkAndResumeStuckUploads(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setSelectedItemId(int id) { |     public void setSelectedItemId(int id) { | ||||||
|         tabLayout.setSelectedItemId(id); |         binding.fragmentMainNavTabLayout.setSelectedItemId(id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setUpPager() { |     private void setUpPager() { | ||||||
|         tabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { |         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { | ||||||
|             if (!item.getTitle().equals(getString(R.string.more))) { |             if (!item.getTitle().equals(getString(R.string.more))) { | ||||||
|                 // do not change title for more fragment |                 // do not change title for more fragment | ||||||
|                 setTitle(item.getTitle()); |                 setTitle(item.getTitle()); | ||||||
|  | @ -175,7 +194,7 @@ public class MainActivity  extends BaseActivity | ||||||
| 
 | 
 | ||||||
|     private void setUpLoggedOutPager() { |     private void setUpLoggedOutPager() { | ||||||
|         loadFragment(ExploreFragment.newInstance(),false); |         loadFragment(ExploreFragment.newInstance(),false); | ||||||
|         tabLayout.setOnNavigationItemSelectedListener(item -> { |         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { | ||||||
|             if (!item.getTitle().equals(getString(R.string.more))) { |             if (!item.getTitle().equals(getString(R.string.more))) { | ||||||
|                 // do not change title for more fragment |                 // do not change title for more fragment | ||||||
|                 setTitle(item.getTitle()); |                 setTitle(item.getTitle()); | ||||||
|  | @ -237,11 +256,11 @@ public class MainActivity  extends BaseActivity | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void hideTabs() { |     public void hideTabs() { | ||||||
|         tabLayout.setVisibility(View.GONE); |         binding.fragmentMainNavTabLayout.setVisibility(View.GONE); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void showTabs() { |     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 |     @Override | ||||||
|     protected void onPostCreate(@Nullable Bundle savedInstanceState) { |     protected void onPostCreate(@Nullable Bundle savedInstanceState) { | ||||||
|         super.onPostCreate(savedInstanceState); |         super.onPostCreate(savedInstanceState); | ||||||
|  | @ -268,7 +315,7 @@ public class MainActivity  extends BaseActivity | ||||||
|     @Override |     @Override | ||||||
|     protected void onSaveInstanceState(Bundle outState) { |     protected void onSaveInstanceState(Bundle outState) { | ||||||
|         super.onSaveInstanceState(outState); |         super.onSaveInstanceState(outState); | ||||||
|         outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem()); |         outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem()); | ||||||
|         outState.putString("activeFragment", activeFragment.name()); |         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() { |     public void toggleLimitedConnectionMode() { | ||||||
|         defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, |         defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, | ||||||
|             !defaultKvStore |             !defaultKvStore | ||||||
|  | @ -356,10 +418,8 @@ public class MainActivity  extends BaseActivity | ||||||
|             viewUtilWrapper |             viewUtilWrapper | ||||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); |                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); | ||||||
|         } else { |         } else { | ||||||
|             WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork( |             WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), | ||||||
|                 UploadWorker.class.getSimpleName(), |                 ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||||
|                 ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class)); |  | ||||||
| 
 |  | ||||||
|             viewUtilWrapper |             viewUtilWrapper | ||||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); |                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); | ||||||
|         } |         } | ||||||
|  | @ -368,8 +428,6 @@ public class MainActivity  extends BaseActivity | ||||||
|     public void centerMapToPlace(Place place) { |     public void centerMapToPlace(Place place) { | ||||||
|         setSelectedItemId(NavTab.NEARBY.code()); |         setSelectedItemId(NavTab.NEARBY.code()); | ||||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() { |         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 |             @Override | ||||||
|             public void onReady() { |             public void onReady() { | ||||||
|                 nearbyParentFragment.centerMapToPlace(place); |                 nearbyParentFragment.centerMapToPlace(place); | ||||||
|  | @ -390,8 +448,11 @@ public class MainActivity  extends BaseActivity | ||||||
| 
 | 
 | ||||||
|         if ((applicationKvStore.getBoolean("firstrun", true)) && |         if ((applicationKvStore.getBoolean("firstrun", true)) && | ||||||
|             (!applicationKvStore.getBoolean("login_skipped"))) { |             (!applicationKvStore.getBoolean("login_skipped"))) { | ||||||
|  |             defaultKvStore.putBoolean("inAppCameraFirstRun", true); | ||||||
|             WelcomeActivity.startYourself(this); |             WelcomeActivity.startYourself(this); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         retryAllFailedUploads(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -407,7 +468,7 @@ public class MainActivity  extends BaseActivity | ||||||
|      * Public method to show nearby from the reference of this. |      * Public method to show nearby from the reference of this. | ||||||
|      */ |      */ | ||||||
|     public void showNearby() { |     public void showNearby() { | ||||||
|         tabLayout.setSelectedItemId(NavTab.NEARBY.code()); |         binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public enum ActiveFragment { |     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_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 | ||||||
|     private val SWIPE_VELOCITY_THRESHOLD = 1000 |     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) |         return gestureDetector.onTouchEvent(motionEvent) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { | ||||||
| 
 | 
 | ||||||
|     inner class GestureListener : GestureDetector.SimpleOnGestureListener() { |     inner class GestureListener : GestureDetector.SimpleOnGestureListener() { | ||||||
| 
 | 
 | ||||||
|         override fun onDown(e: MotionEvent?): Boolean { |         override fun onDown(e: MotionEvent): Boolean { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.bumptech.glide.Glide | import com.bumptech.glide.Glide | ||||||
| import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView | import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView | ||||||
| import fr.free.nrw.commons.R | 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 | ||||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY | 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.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY | ||||||
|  | @ -85,6 +86,8 @@ class ImageAdapter( | ||||||
|      */ |      */ | ||||||
|     private var actionableImagesMap: TreeMap<Int, Image> = TreeMap() |     private var actionableImagesMap: TreeMap<Int, Image> = TreeMap() | ||||||
| 
 | 
 | ||||||
|  |     private var uploadingContributionList: List<Contribution> = ArrayList() | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Stores already added positions of actionable images |      * Stores already added positions of actionable images | ||||||
|      */ |      */ | ||||||
|  | @ -119,6 +122,7 @@ class ImageAdapter( | ||||||
|      * Bind View holder, load image, selected view, click listeners. |      * Bind View holder, load image, selected view, click listeners. | ||||||
|      */ |      */ | ||||||
|     override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { |     override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { | ||||||
|  | 
 | ||||||
|         var image=images[position] |         var image=images[position] | ||||||
|         holder.image.setImageDrawable (null) |         holder.image.setImageDrawable (null) | ||||||
|         if (context.contentResolver.getType(image.uri) == null) { |         if (context.contentResolver.getType(image.uri) == null) { | ||||||
|  | @ -151,13 +155,12 @@ class ImageAdapter( | ||||||
| 
 | 
 | ||||||
|             val isSelected = selectedIndex != -1 |             val isSelected = selectedIndex != -1 | ||||||
|             if (isSelected) { |             if (isSelected) { | ||||||
|                 holder.itemSelected(selectedImages.size) |                 holder.itemSelected() | ||||||
|             } else { |             } else { | ||||||
|                 holder.itemUnselected() |                 holder.itemUnselected() | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             imageLoader.queryAndSetView( |             imageLoader.queryAndSetView( | ||||||
|                 holder, image, ioDispatcher, defaultDispatcher |                 holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList | ||||||
|             ) |             ) | ||||||
|             scope.launch { |             scope.launch { | ||||||
|                 val sharedPreferences: SharedPreferences = |                 val sharedPreferences: SharedPreferences = | ||||||
|  | @ -168,15 +171,17 @@ class ImageAdapter( | ||||||
|                     // If the position is not already visited, that means the position is new then |                     // If the position is not already visited, that means the position is new then | ||||||
|                     // finds the next actionable image position from all images |                     // finds the next actionable image position from all images | ||||||
|                     if (!alreadyAddedPositions.contains(position)) { |                     if (!alreadyAddedPositions.contains(position)) { | ||||||
|                         processThumbnailForActionedImage(holder, position) |                         processThumbnailForActionedImage(holder, position, uploadingContributionList) | ||||||
| 
 | 
 | ||||||
|                     // If the position is already visited, that means the image is already present |                     // 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 |                     // inside map, so it will fetch the image from the map and load in the holder | ||||||
|                     } else { |                     } else { | ||||||
|                         val actionableImages: List<Image> = ArrayList(actionableImagesMap.values) |                         val actionableImages: List<Image> = ArrayList(actionableImagesMap.values) | ||||||
|                         image = actionableImages[position] |                         if(actionableImages.size > position) { | ||||||
|                         Glide.with(holder.image).load(image.uri) |                             image = actionableImages[position] | ||||||
|                             .thumbnail(0.3f).into(holder.image) |                             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 |                 // If switch is turned off, it just fetches the image from all images without any | ||||||
|  | @ -204,11 +209,12 @@ class ImageAdapter( | ||||||
|      */ |      */ | ||||||
|     suspend fun processThumbnailForActionedImage( |     suspend fun processThumbnailForActionedImage( | ||||||
|         holder: ImageViewHolder, |         holder: ImageViewHolder, | ||||||
|         position: Int |         position: Int, | ||||||
|  |         uploadingContributionList: List<Contribution> | ||||||
|     ) { |     ) { | ||||||
|         val next = imageLoader.nextActionableImage( |         val next = imageLoader.nextActionableImage( | ||||||
|             allImages, ioDispatcher, defaultDispatcher, |             allImages, ioDispatcher, defaultDispatcher, | ||||||
|             nextImagePosition |             nextImagePosition, uploadingContributionList | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         // If next actionable image is found, saves it, as the the search for |         // If next actionable image is found, saves it, as the the search for | ||||||
|  | @ -328,12 +334,13 @@ class ImageAdapter( | ||||||
|     /** |     /** | ||||||
|      * Initialize the data set. |      * 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 |         allImages = fixedImages | ||||||
|         val oldImageList:ArrayList<Image> = images |         val oldImageList:ArrayList<Image> = images | ||||||
|         val newImageList:ArrayList<Image> = ArrayList(newImages) |         val newImageList:ArrayList<Image> = ArrayList(newImages) | ||||||
|         actionableImagesMap = emptyMap |         actionableImagesMap = emptyMap | ||||||
|         alreadyAddedPositions = ArrayList() |         alreadyAddedPositions = ArrayList() | ||||||
|  |         uploadingContributionList = uploadedImages | ||||||
|         nextImagePosition = 0 |         nextImagePosition = 0 | ||||||
|         reachedEndOfFolder = false |         reachedEndOfFolder = false | ||||||
|         selectedImages = ArrayList() |         selectedImages = ArrayList() | ||||||
|  | @ -355,15 +362,56 @@ class ImageAdapter( | ||||||
|     /** |     /** | ||||||
|      * Refresh the data in the adapter |      * 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 |         numberOfSelectedImagesMarkedAsNotForUpload = 0 | ||||||
|         selectedImages.clear() |  | ||||||
|         images.clear() |         images.clear() | ||||||
|         selectedImages = arrayListOf() |         selectedImages = arrayListOf() | ||||||
|         init(newImages, fixedImages, TreeMap()) |         init(newImages, fixedImages, TreeMap(),uploadingImages) | ||||||
|         notifyDataSetChanged() |         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. |      * 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) { |     class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { | ||||||
|         val image: ImageView = itemView.findViewById(R.id.image_thumbnail) |         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 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 notForUploadGroup: Group = itemView.findViewById(R.id.not_for_upload_group) | ||||||
|         private val selectedGroup: Group = itemView.findViewById(R.id.selected_group) |         private val selectedGroup: Group = itemView.findViewById(R.id.selected_group) | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * Item selected view. |          * Item selected view. | ||||||
|          */ |          */ | ||||||
|         fun itemSelected(index: Int) { |         fun itemSelected() { | ||||||
|             selectedGroup.visibility = View.VISIBLE |             selectedGroup.visibility = View.VISIBLE | ||||||
|             selectedNumber.text = index.toString() |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|  | @ -434,6 +481,13 @@ class ImageAdapter( | ||||||
|             uploadedGroup.visibility = View.VISIBLE |             uploadedGroup.visibility = View.VISIBLE | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Item is uploading | ||||||
|  |          */ | ||||||
|  |         fun itemUploading() { | ||||||
|  |             uploadingGroup.visibility = View.VISIBLE | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         /** |         /** | ||||||
|          * Item is not for upload view |          * Item is not for upload view | ||||||
|          */ |          */ | ||||||
|  | @ -452,6 +506,13 @@ class ImageAdapter( | ||||||
|             return notForUploadGroup.visibility == View.VISIBLE |             return notForUploadGroup.visibility == View.VISIBLE | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Item is not uploading | ||||||
|  |          */ | ||||||
|  |         fun itemNotUploading() { | ||||||
|  |             uploadingGroup.visibility = View.GONE | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         /** |         /** | ||||||
|          * Item Not Uploaded view. |          * Item Not Uploaded view. | ||||||
|          */ |          */ | ||||||
|  | @ -513,4 +574,4 @@ class ImageAdapter( | ||||||
|         return images[position].date |         return images[position].date | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
| import fr.free.nrw.commons.utils.CustomSelectorUtils | import fr.free.nrw.commons.utils.CustomSelectorUtils | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
| import java.io.File | import java.io.File | ||||||
|  | import java.lang.Integer.max | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -66,6 +67,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|      */ |      */ | ||||||
|     private lateinit var prefs: SharedPreferences |     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. |      * View Model Factory. | ||||||
|      */ |      */ | ||||||
|  | @ -95,6 +112,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|      */ |      */ | ||||||
|     var imageFragment: ImageFragment? = null |     var imageFragment: ImageFragment? = null | ||||||
| 
 | 
 | ||||||
|  |     private var progressDialogText:String="" | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * onCreate Activity, sets theme, initialises the view model, setup view. |      * onCreate Activity, sets theme, initialises the view model, setup view. | ||||||
|      */ |      */ | ||||||
|  | @ -140,7 +159,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|                 data!! |                 data!! | ||||||
|                     .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!! |                     .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!! | ||||||
|             val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false) |             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()) |             markAsNotForUpload(arrayListOf()) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         var i = 0 | 
 | ||||||
|         while (i < selectedImages.size) { |         val iterator = selectedImages.iterator() | ||||||
|             val path = selectedImages[i].path |         while (iterator.hasNext()) { | ||||||
|  |             val image = iterator.next() | ||||||
|  |             val path = image.path | ||||||
|             val file = File(path) |             val file = File(path) | ||||||
|             if (!file.exists()) { |             if (!file.exists()) { | ||||||
|                 selectedImages.removeAt(i) |                 iterator.remove() | ||||||
|                 i-- |  | ||||||
|             } |             } | ||||||
|             i++ |  | ||||||
|         } |         } | ||||||
|         markAsNotForUpload(selectedImages) |         markAsNotForUpload(selectedImages) | ||||||
|  |         toolbarBinding.imageLimitError.visibility = View.INVISIBLE | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -221,56 +241,63 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|      */ |      */ | ||||||
|     private fun insertIntoNotForUpload(images: ArrayList<Image>) { |     private fun insertIntoNotForUpload(images: ArrayList<Image>) { | ||||||
|         scope.launch { |         scope.launch { | ||||||
|  |             withContext(Dispatchers.Main) { | ||||||
|  |                 imageFragment?.showMarkUnmarkProgressDialog(text = progressDialogText) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             var allImagesAlreadyNotForUpload = true |             var allImagesAlreadyNotForUpload = true | ||||||
|             images.forEach { |             images.forEach { image -> | ||||||
|                 val imageSHA1 = CustomSelectorUtils.getImageSHA1( |                 val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||||
|                     it.uri, |                     image.uri, | ||||||
|                     ioDispatcher, |                     ioDispatcher, | ||||||
|                     fileUtilsWrapper, |                     fileUtilsWrapper, | ||||||
|                     contentResolver |                     contentResolver | ||||||
|                 ) |                 ) | ||||||
|                 val exists = notForUploadStatusDao.find(imageSHA1) |                 val exists = notForUploadStatusDao.find(imageSHA1) | ||||||
| 
 |  | ||||||
|                 // If image exists in not for upload table make allImagesAlreadyNotForUpload false |  | ||||||
|                 if (exists < 1) { |                 if (exists < 1) { | ||||||
|                     allImagesAlreadyNotForUpload = false |                     allImagesAlreadyNotForUpload = false | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // if all images is not already marked as not for upload, insert all images in |  | ||||||
|             // not for upload table |  | ||||||
|             if (!allImagesAlreadyNotForUpload) { |             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( |                     val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||||
|                         it.uri, |                         image.uri, | ||||||
|                         ioDispatcher, |                         ioDispatcher, | ||||||
|                         fileUtilsWrapper, |                         fileUtilsWrapper, | ||||||
|                         contentResolver |                         contentResolver | ||||||
|                     ) |                     ) | ||||||
|                     notForUploadStatusDao.insert( |                     notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1)) | ||||||
|                         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 { |             } else { | ||||||
|                 images.forEach { |                 images.forEach { image -> | ||||||
|                     val imageSHA1 = CustomSelectorUtils.getImageSHA1( |                     val imageSHA1 = CustomSelectorUtils.getImageSHA1( | ||||||
|                         it.uri, |                         image.uri, | ||||||
|                         ioDispatcher, |                         ioDispatcher, | ||||||
|                         fileUtilsWrapper, |                         fileUtilsWrapper, | ||||||
|                         contentResolver |                         contentResolver | ||||||
|                     ) |                     ) | ||||||
|                     notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1) |                     notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1) | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  |                 withContext(Dispatchers.Main) { | ||||||
|  |                     imageFragment?.refresh() | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             imageFragment!!.refresh() |             withContext(Dispatchers.Main) { | ||||||
|             val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout) |                 imageFragment?.dismissMarkUnmarkProgressDialog() | ||||||
|             bottomLayout.visibility = View.GONE |                 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. |      * Change the title of the toolbar. | ||||||
|      */ |      */ | ||||||
|     private fun changeTitle(title: String) { |     private fun changeTitle(title: String, selectedImageCount:Int) { | ||||||
|         val titleText = findViewById<TextView>(R.id.title) |         if (title.isNotEmpty()){ | ||||||
|         if (titleText != null) { |             val titleText = findViewById<TextView>(R.id.title) | ||||||
|             titleText.text = 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() { |     private fun setUpToolbar() { | ||||||
|         val back: ImageButton = findViewById(R.id.back) |         val back: ImageButton = findViewById(R.id.back) | ||||||
|         back.setOnClickListener { onBackPressed() } |         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) |             .addToBackStack(null) | ||||||
|             .commit() |             .commit() | ||||||
| 
 | 
 | ||||||
|         changeTitle(folderName) |         changeTitle(folderName, 0) | ||||||
| 
 | 
 | ||||||
|         bucketId = folderId |         bucketId = folderId | ||||||
|         bucketName = folderName |         bucketName = folderName | ||||||
|  | @ -323,8 +361,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|         selectedNotForUploadImages: Int |         selectedNotForUploadImages: Int | ||||||
|     ) { |     ) { | ||||||
|         viewModel.selectedImages.value = selectedImages |         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.isEnabled = false | ||||||
|             bottomSheetBinding.upload.alpha = 0.5f |             bottomSheetBinding.upload.alpha = 0.5f | ||||||
|         } else { |         } else { | ||||||
|  | @ -334,8 +385,14 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
| 
 | 
 | ||||||
|         bottomSheetBinding.notForUpload.text = |         bottomSheetBinding.notForUpload.text = | ||||||
|             when (selectedImages.size == selectedNotForUploadImages) { |             when (selectedImages.size == selectedNotForUploadImages) { | ||||||
|                 true -> getString(R.string.unmark_as_not_for_upload) |                 true -> { | ||||||
|                 else -> getString(R.string.mark_as_not_for_upload) |                     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) |         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. |      * Get the selected images. Remove any non existent file, forward the data to finish selector. | ||||||
|      */ |      */ | ||||||
|     fun onDone() { |     fun onDone() { | ||||||
|         val selectedImages = viewModel.selectedImages.value |             val selectedImages = viewModel.selectedImages.value | ||||||
|         if (selectedImages.isNullOrEmpty()) { |             if (selectedImages.isNullOrEmpty()) { | ||||||
|             finishPickImages(arrayListOf()) |                 finishPickImages(arrayListOf()) | ||||||
|             return |                 return | ||||||
|         } |  | ||||||
|         var i = 0 |  | ||||||
|         while (i < selectedImages.size) { |  | ||||||
|             val path = selectedImages[i].path |  | ||||||
|             val file = File(path) |  | ||||||
|             if (!file.exists()) { |  | ||||||
|                 selectedImages.removeAt(i) |  | ||||||
|                 i-- |  | ||||||
|             } |             } | ||||||
|             i++ |             var i = 0 | ||||||
|         } |             while (i < selectedImages.size) { | ||||||
|         finishPickImages(selectedImages) |                 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) |         val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) | ||||||
|         if (fragment != null && fragment is FolderFragment) { |         if (fragment != null && fragment is FolderFragment) { | ||||||
|             isImageFragmentOpen = false |             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 |      * On activity destroy | ||||||
|      * If image fragment is open, overwrite its attributes otherwise discard the values. |      * If image fragment is open, overwrite its attributes otherwise discard the values. | ||||||
|  | @ -426,4 +497,4 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|         const val FOLDER_NAME: String = "FolderName" |         const val FOLDER_NAME: String = "FolderName" | ||||||
|         const val ITEM_ID: String = "ItemId" |         const val ITEM_ID: String = "ItemId" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -93,7 +93,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { | ||||||
|      */ |      */ | ||||||
|     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) |         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) | ||||||
|         folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) |         folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) | ||||||
|         gridLayoutManager = GridLayoutManager(context, columnCount()) |         gridLayoutManager = GridLayoutManager(context, columnCount()) | ||||||
|         selectorRV = binding?.selectorRv |         selectorRV = binding?.selectorRv | ||||||
|         loader = binding?.loader |         loader = binding?.loader | ||||||
|  | @ -159,4 +159,4 @@ class FolderFragment : CommonsDaggerSupportFragment() { | ||||||
|         return 2 |         return 2 | ||||||
|         // todo change column count depending on the orientation of the device. |         // todo change column count depending on the orientation of the device. | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,7 +11,9 @@ import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import kotlinx.coroutines.withContext | import kotlinx.coroutines.withContext | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.util.* | import java.util.Calendar | ||||||
|  | import java.util.Date | ||||||
|  | import java.util.Locale | ||||||
| import kotlin.coroutines.CoroutineContext | 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) { |                 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 uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) | ||||||
| 
 | 
 | ||||||
|                     val calendar = Calendar.getInstance() |                     val calendar = Calendar.getInstance() | ||||||
|  | @ -130,4 +138,4 @@ class ImageFileLoader(val context: Context) : CoroutineScope{ | ||||||
|      * Sha1 for image (original image). |      * Sha1 for image (original image). | ||||||
|      * |      * | ||||||
|      */ |      */ | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,37 +9,41 @@ import android.view.View | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import android.widget.ProgressBar | import android.widget.ProgressBar | ||||||
| import android.widget.Switch | import android.widget.Switch | ||||||
|  | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout | import androidx.constraintlayout.widget.ConstraintLayout | ||||||
| import androidx.lifecycle.Observer | import androidx.lifecycle.Observer | ||||||
| import androidx.lifecycle.ViewModelProvider | import androidx.lifecycle.ViewModelProvider | ||||||
| import androidx.recyclerview.widget.GridLayoutManager | import androidx.recyclerview.widget.GridLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | 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.NotForUploadStatusDao | ||||||
| import fr.free.nrw.commons.customselector.database.UploadedStatusDao | import fr.free.nrw.commons.customselector.database.UploadedStatusDao | ||||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper | 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.CUSTOM_SELECTOR_PREFERENCE_KEY | ||||||
| import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_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.ImageSelectListener | ||||||
|  | import fr.free.nrw.commons.customselector.listeners.PassDataListener | ||||||
| import fr.free.nrw.commons.customselector.listeners.RefreshUIListener | import fr.free.nrw.commons.customselector.listeners.RefreshUIListener | ||||||
| import fr.free.nrw.commons.customselector.model.CallbackStatus | import fr.free.nrw.commons.customselector.model.CallbackStatus | ||||||
| import fr.free.nrw.commons.customselector.model.Image | import fr.free.nrw.commons.customselector.model.Image | ||||||
| import fr.free.nrw.commons.customselector.model.Result | import fr.free.nrw.commons.customselector.model.Result | ||||||
| import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter | import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter | ||||||
| import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding | 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.di.CommonsDaggerSupportFragment | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.upload.FileProcessor | import fr.free.nrw.commons.upload.FileProcessor | ||||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
|  | import io.reactivex.schedulers.Schedulers | ||||||
| import java.util.* | import java.util.* | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import kotlin.collections.ArrayList |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Custom Selector Image Fragment. |  * Custom Selector Image Fragment. | ||||||
|  */ |  */ | ||||||
| class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener { | class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener { | ||||||
| 
 | 
 | ||||||
|     private var _binding: FragmentCustomSelectorBinding? = null |     private var _binding: FragmentCustomSelectorBinding? = null | ||||||
|     private val binding get() = _binding |     private val binding get() = _binding | ||||||
|  | @ -57,7 +61,7 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|     /** |     /** | ||||||
|      * View model for images. |      * View model for images. | ||||||
|      */ |      */ | ||||||
|     private var  viewModel: CustomSelectorViewModel? = null |     private var viewModel: CustomSelectorViewModel? = null | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * View Elements. |      * View Elements. | ||||||
|  | @ -99,6 +103,10 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|      */ |      */ | ||||||
|     private var progressLayout: ConstraintLayout? = null |     private var progressLayout: ConstraintLayout? = null | ||||||
| 
 | 
 | ||||||
|  |     private lateinit var progressDialog: AlertDialog | ||||||
|  |     private lateinit var progressDialogLayout: ProgressDialogBinding | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * NotForUploadStatus Dao class for database operations |      * NotForUploadStatus Dao class for database operations | ||||||
|      */ |      */ | ||||||
|  | @ -129,6 +137,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|     @Inject |     @Inject | ||||||
|     lateinit var mediaClient: MediaClient |     lateinit var mediaClient: MediaClient | ||||||
| 
 | 
 | ||||||
|  |     @Inject | ||||||
|  |     lateinit var contributionDao: ContributionDao | ||||||
|  | 
 | ||||||
|     companion object { |     companion object { | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|  | @ -163,7 +174,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|         bucketId = arguments?.getLong(BUCKET_ID) |         bucketId = arguments?.getLong(BUCKET_ID) | ||||||
|         lastItemId = arguments?.getLong(LAST_ITEM_ID, 0) |         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. |      * Init imageAdapter, gridLayoutManger. | ||||||
|      * SetUp recycler view. |      * 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) |         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) | ||||||
|         imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) |         imageAdapter = | ||||||
|         gridLayoutManager = GridLayoutManager(context,getSpanCount()) |             ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) | ||||||
|         with(binding?.selectorRv){ |         gridLayoutManager = GridLayoutManager(context, getSpanCount()) | ||||||
|  |         with(binding?.selectorRv) { | ||||||
|             this?.layoutManager = gridLayoutManager |             this?.layoutManager = gridLayoutManager | ||||||
|             this?.setHasFixedSize(true) |             this?.setHasFixedSize(true) | ||||||
|             this?.adapter = imageAdapter |             this?.adapter = imageAdapter | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         viewModel?.result?.observe(viewLifecycleOwner, Observer{ |         viewModel?.result?.observe(viewLifecycleOwner, Observer { | ||||||
|             handleResult(it) |             handleResult(it) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -194,9 +212,16 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
| 
 | 
 | ||||||
|         val sharedPreferences: SharedPreferences = |         val sharedPreferences: SharedPreferences = | ||||||
|             requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE) |             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 |         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 |         return binding?.root | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -217,7 +242,8 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|             editor.apply() |             editor.apply() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         imageAdapter.init(allImages, allImages, TreeMap()) |         val uploadingContributions = getUploadingContributions() | ||||||
|  |         imageAdapter.init(allImages, allImages, TreeMap(), uploadingContributions) | ||||||
|         imageAdapter.notifyDataSetChanged() |         imageAdapter.notifyDataSetChanged() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -236,13 +262,15 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|     /** |     /** | ||||||
|      * Handle view model result. |      * Handle view model result. | ||||||
|      */ |      */ | ||||||
|     private fun handleResult(result:Result){ |     private fun handleResult(result: Result) { | ||||||
|         if(result.status is CallbackStatus.SUCCESS){ |         if (result.status is CallbackStatus.SUCCESS) { | ||||||
|             val images = result.images |             val images = result.images | ||||||
|             if(images.isNotEmpty()) { | 
 | ||||||
|  |             val uploadingContributions = getUploadingContributions() | ||||||
|  |             if (images.isNotEmpty()) { | ||||||
|                 filteredImages = ImageHelper.filterImages(images, bucketId) |                 filteredImages = ImageHelper.filterImages(images, bucketId) | ||||||
|                 allImages = ArrayList(filteredImages) |                 allImages = ArrayList(filteredImages) | ||||||
|                 imageAdapter.init(filteredImages, allImages, TreeMap()) |                 imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) | ||||||
|                 selectorRV?.let { |                 selectorRV?.let { | ||||||
|                     it.visibility = View.VISIBLE |                     it.visibility = View.VISIBLE | ||||||
|                     lastItemId?.let { pos -> |                     lastItemId?.let { pos -> | ||||||
|  | @ -250,18 +278,18 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData | ||||||
|                             .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) |                             .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } else { | ||||||
|             else{ |  | ||||||
|                 binding?.emptyText?.let { |                 binding?.emptyText?.let { | ||||||
|                     it.visibility = View.VISIBLE |                     it.visibility = View.VISIBLE | ||||||
|                 } |                 } | ||||||
|                 selectorRV?.let{ |                 selectorRV?.let { | ||||||
|                     it.visibility = View.GONE |                     it.visibility = View.GONE | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         loader?.let { |         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() { |     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 |      * Passes selected images and other information from Activity to Fragment and connects it with | ||||||
|      * the adapter |      * the adapter | ||||||
|      */ |      */ | ||||||
|     override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean){ |     override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) { | ||||||
|         imageAdapter.setSelectedImages(selectedImages) |         imageAdapter.setSelectedImages(selectedImages) | ||||||
| 
 | 
 | ||||||
|  |         val uploadingContributions = getUploadingContributions() | ||||||
|  | 
 | ||||||
|         if (!showAlreadyActionedImages && shouldRefresh) { |         if (!showAlreadyActionedImages && shouldRefresh) { | ||||||
|             imageAdapter.init(filteredImages, allImages, TreeMap()) |             imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions) | ||||||
|             imageAdapter.setSelectedImages(selectedImages) |             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.Context | ||||||
| import android.content.SharedPreferences | import android.content.SharedPreferences | ||||||
| import android.net.Uri | 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.NotForUploadStatusDao | ||||||
| import fr.free.nrw.commons.customselector.database.UploadedStatus | import fr.free.nrw.commons.customselector.database.UploadedStatus | ||||||
| import fr.free.nrw.commons.customselector.database.UploadedStatusDao | import fr.free.nrw.commons.customselector.database.UploadedStatusDao | ||||||
|  | @ -75,7 +76,8 @@ class ImageLoader @Inject constructor( | ||||||
|         holder: ImageViewHolder, |         holder: ImageViewHolder, | ||||||
|         image: Image, |         image: Image, | ||||||
|         ioDispatcher: CoroutineDispatcher, |         ioDispatcher: CoroutineDispatcher, | ||||||
|         defaultDispatcher: CoroutineDispatcher |         defaultDispatcher: CoroutineDispatcher, | ||||||
|  |         uploadedContributionsList : List<Contribution> | ||||||
|     ) { |     ) { | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|  | @ -84,6 +86,7 @@ class ImageLoader @Inject constructor( | ||||||
|         mapHolderImage[holder] = image |         mapHolderImage[holder] = image | ||||||
|         holder.itemNotUploaded() |         holder.itemNotUploaded() | ||||||
|         holder.itemForUpload() |         holder.itemForUpload() | ||||||
|  |         holder.itemNotUploading() | ||||||
| 
 | 
 | ||||||
|         scope.launch { |         scope.launch { | ||||||
|             var result: Result = Result.NOTFOUND |             var result: Result = Result.NOTFOUND | ||||||
|  | @ -214,6 +217,17 @@ class ImageLoader @Inject constructor( | ||||||
|                     holder.itemNotForUpload() |                     holder.itemNotForUpload() | ||||||
|                 } else holder.itemForUpload() |                 } 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( |     suspend fun nextActionableImage( | ||||||
|         allImages: List<Image>, ioDispatcher: CoroutineDispatcher, |         allImages: List<Image>, ioDispatcher: CoroutineDispatcher, | ||||||
|         defaultDispatcher: CoroutineDispatcher, |         defaultDispatcher: CoroutineDispatcher, | ||||||
|         nextImagePosition: Int |         nextImagePosition: Int, | ||||||
|  |         currentlyUploadingImages: List<Contribution> | ||||||
|     ): Int { |     ): Int { | ||||||
|         var next: Int |         var next: Int | ||||||
| 
 |  | ||||||
|         // Traversing from given position to the end |         // Traversing from given position to the end | ||||||
|         for (i in nextImagePosition until allImages.size){ |         for (i in nextImagePosition until allImages.size){ | ||||||
|             val it = allImages[i] |             val currentImage = allImages[i] | ||||||
|             val imageSHA1: String = when (mapImageSHA1[it.uri] != null) { | 
 | ||||||
|                 true -> mapImageSHA1[it.uri]!! |             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( |                 else -> CustomSelectorUtils.getImageSHA1( | ||||||
|                     it.uri, |                     currentImage.uri, | ||||||
|                     ioDispatcher, |                     ioDispatcher, | ||||||
|                     fileUtilsWrapper, |                     fileUtilsWrapper, | ||||||
|                     context.contentResolver |                     context.contentResolver | ||||||
|  | @ -253,7 +272,7 @@ class ImageLoader @Inject constructor( | ||||||
|                 // If the image is not present in the already uploaded table, checks for its |                 // If the image is not present in the already uploaded table, checks for its | ||||||
|                 // modified SHA1 in already uploaded table |                 // modified SHA1 in already uploaded table | ||||||
|                 if (next <= 0) { |                 if (next <= 0) { | ||||||
|                     val modifiedImageSha1 = getSHA1(it, defaultDispatcher) |                     val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher) | ||||||
|                     next = uploadedStatusDao.findByModifiedImageSHA1( |                     next = uploadedStatusDao.findByModifiedImageSHA1( | ||||||
|                         modifiedImageSha1, |                         modifiedImageSha1, | ||||||
|                         true |                         true | ||||||
|  | @ -360,4 +379,4 @@ class ImageLoader @Inject constructor( | ||||||
|         const val INVALIDATE_DAY_COUNT: Long = 7 |         const val INVALIDATE_DAY_COUNT: Long = 7 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ import androidx.room.TypeConverters | ||||||
| import fr.free.nrw.commons.contributions.Contribution | import fr.free.nrw.commons.contributions.Contribution | ||||||
| import fr.free.nrw.commons.contributions.ContributionDao | import fr.free.nrw.commons.contributions.ContributionDao | ||||||
| import fr.free.nrw.commons.customselector.database.* | 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.ReviewDao | ||||||
| import fr.free.nrw.commons.review.ReviewEntity | import fr.free.nrw.commons.review.ReviewEntity | ||||||
| import fr.free.nrw.commons.upload.depicts.Depicts | 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 |  * 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) | @TypeConverters(Converters::class) | ||||||
| abstract class AppDatabase : RoomDatabase() { | abstract class AppDatabase : RoomDatabase() { | ||||||
|     abstract fun contributionDao(): ContributionDao |     abstract fun contributionDao(): ContributionDao | ||||||
|  |     abstract fun PlaceDao(): PlaceDao | ||||||
|     abstract fun DepictsDao(): DepictsDao; |     abstract fun DepictsDao(): DepictsDao; | ||||||
|     abstract fun UploadedStatusDao(): UploadedStatusDao; |     abstract fun UploadedStatusDao(): UploadedStatusDao; | ||||||
|     abstract fun NotForUploadStatusDao(): NotForUploadStatusDao |     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.contributions.ChunkInfo; | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection; | import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||||
| import fr.free.nrw.commons.location.LatLng; | 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.WikidataPlace; | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | ||||||
|  | import java.lang.reflect.Type; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | @ -134,6 +136,18 @@ public class Converters { | ||||||
|         return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {}); |         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) { |     private static String writeObjectToString(Object object) { | ||||||
|         return object == null ? null : getGson().toJson(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.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.actions.PageEditClient; | 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.notification.NotificationHelper; | ||||||
| import fr.free.nrw.commons.review.ReviewController; | import fr.free.nrw.commons.review.ReviewController; | ||||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | import fr.free.nrw.commons.utils.ViewUtilWrapper; | ||||||
|  | @ -66,7 +67,13 @@ public class DeleteHelper { | ||||||
| 
 | 
 | ||||||
|         return delete(media, reason) |         return delete(media, reason) | ||||||
|                 .flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result))) |                 .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) |         return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary) | ||||||
|                 .flatMap(result -> { |             .onErrorResumeNext(throwable -> { | ||||||
|                     if (result) { |                 if (throwable instanceof InvalidLoginTokenException) { | ||||||
|                         return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); |                     return Observable.error(throwable); | ||||||
|                     } |                 } | ||||||
|                     throw new RuntimeException("Failed to nominate for deletion"); |                 return Observable.error(throwable); | ||||||
|                 }).flatMap(result -> { |             }) | ||||||
|                     if (result) { |             .flatMap(result -> { | ||||||
|                         return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary); |                 if (result) { | ||||||
|                     } |                     return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); | ||||||
|                     throw new RuntimeException("Failed to nominate for deletion"); |                 } | ||||||
|                 }).flatMap(result -> { |                 return Observable.error(new RuntimeException("Failed to nominate for deletion")); | ||||||
|                     if (result) { |             }) | ||||||
|                         return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary); |             .flatMap(result -> { | ||||||
|                     } |                 if (result) { | ||||||
|                     throw new RuntimeException("Failed to nominate for deletion"); |                     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) { |     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) -> { |         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) + " "; |             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()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(aBoolean -> { |                 .subscribe(aBoolean -> { | ||||||
|                     if (aBoolean) { |                     reviewCallback.onSuccess(); | ||||||
|                         reviewCallback.onSuccess(); |                 }, throwable -> { | ||||||
|  |                     if (throwable instanceof InvalidLoginTokenException) { | ||||||
|  |                         reviewCallback.onTokenException((InvalidLoginTokenException) throwable); | ||||||
|                     } else { |                     } else { | ||||||
|                         reviewCallback.onFailure(); |                         reviewCallback.onFailure(); | ||||||
|                     } |                     } | ||||||
|  |                     reviewCallback.enableButtons(); | ||||||
|                 }); |                 }); | ||||||
| 
 |  | ||||||
|         }); |         }); | ||||||
|         alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure()); |         alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure()); | ||||||
|         d = alert.create(); |         d = alert.create(); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ package fr.free.nrw.commons.delete; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| 
 | 
 | ||||||
| import org.wikipedia.util.DateUtil; | import fr.free.nrw.commons.utils.DateUtil; | ||||||
| 
 | 
 | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  |  | ||||||
|  | @ -1,16 +1,21 @@ | ||||||
| package fr.free.nrw.commons.description | package fr.free.nrw.commons.description | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| import android.app.ProgressDialog | import android.app.ProgressDialog | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
|  | import android.speech.RecognizerIntent | ||||||
| import android.view.View | import android.view.View | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import fr.free.nrw.commons.CommonsApplication | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.R | 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.databinding.ActivityDescriptionEditBinding | ||||||
| import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION | 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.description.EditDescriptionConstants.WIKITEXT | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||||
| import fr.free.nrw.commons.settings.Prefs | import fr.free.nrw.commons.settings.Prefs | ||||||
|  | @ -18,8 +23,13 @@ import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetail | import fr.free.nrw.commons.upload.UploadMediaDetail | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.functions.Consumer | ||||||
|  | import io.reactivex.schedulers.Schedulers | ||||||
|  | import timber.log.Timber | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Activity for populating and editing existing description and caption |  * Activity for populating and editing existing description and caption | ||||||
|  */ |  */ | ||||||
|  | @ -40,6 +50,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
|      */ |      */ | ||||||
|     var wikiText: String? = null |     var wikiText: String? = null | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Media object | ||||||
|  |      */ | ||||||
|  |     var media: Media? = null | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Saved language |      * Saved language | ||||||
|      */ |      */ | ||||||
|  | @ -55,6 +70,15 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
| 
 | 
 | ||||||
|     private lateinit var binding: ActivityDescriptionEditBinding |     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?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
| 
 | 
 | ||||||
|  | @ -62,13 +86,21 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
|         setContentView(binding.root) |         setContentView(binding.root) | ||||||
| 
 | 
 | ||||||
|         val bundle = intent.extras |         val bundle = intent.extras | ||||||
|         val descriptionAndCaptions: ArrayList<UploadMediaDetail> = | 
 | ||||||
|             bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!! |         if (savedInstanceState != null) { | ||||||
|         wikiText = bundle.getString(WIKITEXT) |             descriptionAndCaptions = savedInstanceState.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION) | ||||||
|         savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!! |             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) |         initRecyclerView(descriptionAndCaptions) | ||||||
| 
 | 
 | ||||||
|         binding.btnAddDescription.setOnClickListener(::onButtonAddDescriptionClicked) |  | ||||||
|         binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked) |         binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked) | ||||||
|         binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked) |         binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked) | ||||||
|     } |     } | ||||||
|  | @ -78,7 +110,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
|      * @param descriptionAndCaptions list of description and caption |      * @param descriptionAndCaptions list of description and caption | ||||||
|      */ |      */ | ||||||
|     private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { |     private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { | ||||||
|         uploadMediaDetailAdapter = UploadMediaDetailAdapter( |         uploadMediaDetailAdapter = UploadMediaDetailAdapter(this, | ||||||
|             savedLanguageValue, descriptionAndCaptions, recentLanguagesDao) |             savedLanguageValue, descriptionAndCaptions, recentLanguagesDao) | ||||||
|         uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> |         uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> | ||||||
|             showInfoAlert( |             showInfoAlert( | ||||||
|  | @ -107,17 +139,20 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
| 
 | 
 | ||||||
|     override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {} |     override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {} | ||||||
| 
 | 
 | ||||||
|     private fun onBackButtonClicked(view: View) { |     /** | ||||||
|         onBackPressed() |      * Adds new language item to RecyclerView | ||||||
|     } |      */ | ||||||
| 
 |     override fun addLanguage() { | ||||||
|     private fun onButtonAddDescriptionClicked(view: View) { |  | ||||||
|         val uploadMediaDetail = UploadMediaDetail() |         val uploadMediaDetail = UploadMediaDetail() | ||||||
|         uploadMediaDetail.isManuallyAdded = true //This was manually added by the user |         uploadMediaDetail.isManuallyAdded = true //This was manually added by the user | ||||||
|         uploadMediaDetailAdapter.addDescription(uploadMediaDetail) |         uploadMediaDetailAdapter.addDescription(uploadMediaDetail) | ||||||
|         rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) |         rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private fun onBackButtonClicked(view: View) { | ||||||
|  |        onBackPressedDispatcher.onBackPressed() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun onSubmitButtonClicked(view: View) { |     private fun onSubmitButtonClicked(view: View) { | ||||||
|         showLoggingProgressBar() |         showLoggingProgressBar() | ||||||
|         val uploadMediaDetails = uploadMediaDetailAdapter.items |         val uploadMediaDetails = uploadMediaDetailAdapter.items | ||||||
|  | @ -151,22 +186,85 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
|                     buffer.append(uploadDetails.languageCode) |                     buffer.append(uploadDetails.languageCode) | ||||||
|                     buffer.append("|1=") |                     buffer.append("|1=") | ||||||
|                     buffer.append(uploadDetails.descriptionText) |                     buffer.append(uploadDetails.descriptionText) | ||||||
|                     buffer.append("}}, ") |                     buffer.append("}}") | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             buffer.replace(", $".toRegex(), "") |             buffer.replace(", $".toRegex(), "") | ||||||
|             buffer.append(descriptionEnd) |             buffer.append(descriptionEnd) | ||||||
|         } |         } | ||||||
|         val returningIntent = Intent() |         editDescription(media!!, buffer.toString(), uploadMediaDetails as ArrayList<UploadMediaDetail>) | ||||||
|         returningIntent.putExtra(UPDATED_WIKITEXT, buffer.toString()) | 
 | ||||||
|         returningIntent.putParcelableArrayListExtra( |  | ||||||
|             LIST_OF_DESCRIPTION_AND_CAPTION, |  | ||||||
|             uploadMediaDetails as ArrayList<out Parcelable?> |  | ||||||
|         ) |  | ||||||
|         setResult(RESULT_OK, returningIntent) |  | ||||||
|         finish() |         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() { |     private fun showLoggingProgressBar() { | ||||||
|         progressDialog = ProgressDialog(this) |         progressDialog = ProgressDialog(this) | ||||||
|         progressDialog!!.isIndeterminate = true |         progressDialog!!.isIndeterminate = true | ||||||
|  | @ -175,4 +273,24 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi | ||||||
|         progressDialog!!.setCanceledOnTouchOutside(false) |         progressDialog!!.setCanceledOnTouchOutside(false) | ||||||
|         progressDialog!!.show() |         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.depictions.WikidataItemDetailsActivity; | ||||||
| import fr.free.nrw.commons.explore.SearchActivity; | import fr.free.nrw.commons.explore.SearchActivity; | ||||||
| import fr.free.nrw.commons.media.ZoomableActivity; | 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.notification.NotificationActivity; | ||||||
| import fr.free.nrw.commons.profile.ProfileActivity; | import fr.free.nrw.commons.profile.ProfileActivity; | ||||||
| import fr.free.nrw.commons.review.ReviewActivity; | import fr.free.nrw.commons.review.ReviewActivity; | ||||||
|  | @ -79,4 +80,7 @@ public abstract class ActivityBuilderModule { | ||||||
| 
 | 
 | ||||||
|     @ContributesAndroidInjector |     @ContributesAndroidInjector | ||||||
|     abstract ZoomableActivity bindZoomableActivity(); |     abstract ZoomableActivity bindZoomableActivity(); | ||||||
|  | 
 | ||||||
|  |     @ContributesAndroidInjector | ||||||
|  |     abstract WikidataFeedback bindWikiFeedback(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,11 @@ package fr.free.nrw.commons.di; | ||||||
| 
 | 
 | ||||||
| import com.google.gson.Gson; | 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.explore.categories.CategoriesModule; | ||||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; | import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; | ||||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; | 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 fr.free.nrw.commons.upload.worker.UploadWorker; | ||||||
| import javax.inject.Singleton; | import javax.inject.Singleton; | ||||||
| 
 | 
 | ||||||
|  | @ -69,6 +70,9 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application | ||||||
| 
 | 
 | ||||||
|     void inject(PicOfDayAppWidget picOfDayAppWidget); |     void inject(PicOfDayAppWidget picOfDayAppWidget); | ||||||
| 
 | 
 | ||||||
|  |     @Singleton | ||||||
|  |     void inject(NearbyController nearbyController); | ||||||
|  | 
 | ||||||
|     Gson gson(); |     Gson gson(); | ||||||
| 
 | 
 | ||||||
|     @Component.Builder |     @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.db.AppDatabase; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
| import fr.free.nrw.commons.location.LocationServiceManager; | 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.review.ReviewDao; | ||||||
| import fr.free.nrw.commons.settings.Prefs; | import fr.free.nrw.commons.settings.Prefs; | ||||||
| import fr.free.nrw.commons.upload.UploadController; | import fr.free.nrw.commons.upload.UploadController; | ||||||
|  | @ -41,7 +42,6 @@ import java.util.Map; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import javax.inject.Singleton; | import javax.inject.Singleton; | ||||||
| import org.wikipedia.AppAdapter; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The Dependency Provider class for Commons Android. |  * The Dependency Provider class for Commons Android. | ||||||
|  | @ -257,8 +257,8 @@ public class CommonsApplicationModule { | ||||||
| 
 | 
 | ||||||
|     @Named("username") |     @Named("username") | ||||||
|     @Provides |     @Provides | ||||||
|     public String provideLoggedInUsername() { |     public String provideLoggedInUsername(SessionManager sessionManager) { | ||||||
|         return Objects.toString(AppAdapter.get().getUserName(), ""); |         return Objects.toString(sessionManager.getUserName(), ""); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|  | @ -276,6 +276,11 @@ public class CommonsApplicationModule { | ||||||
|         return appDatabase.contributionDao(); |         return appDatabase.contributionDao(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Provides | ||||||
|  |     public PlaceDao providesPlaceDao(AppDatabase appDatabase) { | ||||||
|  |         return appDatabase.PlaceDao(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the reference of DepictsDao class. |      * Get the reference of DepictsDao class. | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -7,8 +7,16 @@ import dagger.Module; | ||||||
| import dagger.Provides; | import dagger.Provides; | ||||||
| import fr.free.nrw.commons.BetaConstants; | import fr.free.nrw.commons.BetaConstants; | ||||||
| import fr.free.nrw.commons.BuildConfig; | 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.PageEditClient; | ||||||
| import fr.free.nrw.commons.actions.PageEditInterface; | 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.category.CategoryInterface; | ||||||
| import fr.free.nrw.commons.explore.depictions.DepictsClient; | import fr.free.nrw.commons.explore.depictions.DepictsClient; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | 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.media.WikidataMediaInterface; | ||||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | ||||||
| import fr.free.nrw.commons.mwapi.UserInterface; | 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.review.ReviewInterface; | ||||||
| import fr.free.nrw.commons.upload.UploadInterface; | import fr.free.nrw.commons.upload.UploadInterface; | ||||||
| import fr.free.nrw.commons.upload.WikiBaseInterface; | import fr.free.nrw.commons.upload.WikiBaseInterface; | ||||||
| import fr.free.nrw.commons.upload.depicts.DepictsInterface; | 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.WikidataInterface; | ||||||
|  | import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; | ||||||
|  | import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  | @ -33,31 +45,25 @@ import okhttp3.HttpUrl; | ||||||
| import okhttp3.OkHttpClient; | import okhttp3.OkHttpClient; | ||||||
| import okhttp3.logging.HttpLoggingInterceptor; | import okhttp3.logging.HttpLoggingInterceptor; | ||||||
| import okhttp3.logging.HttpLoggingInterceptor.Level; | import okhttp3.logging.HttpLoggingInterceptor.Level; | ||||||
| import org.wikipedia.csrf.CsrfTokenClient; | import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||||
| import org.wikipedia.dataclient.Service; | import fr.free.nrw.commons.wikidata.GsonUtil; | ||||||
| import org.wikipedia.dataclient.ServiceFactory; |  | ||||||
| import org.wikipedia.dataclient.WikiSite; |  | ||||||
| import org.wikipedia.json.GsonUtil; |  | ||||||
| import org.wikipedia.login.LoginClient; |  | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| @Module | @Module | ||||||
| @SuppressWarnings({"WeakerAccess", "unused"}) | @SuppressWarnings({"WeakerAccess", "unused"}) | ||||||
| public class NetworkingModule { | public class NetworkingModule { | ||||||
|     private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql"; |     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 TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"; | ||||||
| 
 |  | ||||||
|     private static final String TEST_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 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_DATA_WIKI_SITE = "wikidata-wikisite"; | ||||||
|     private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-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_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite"; | ||||||
| 
 | 
 | ||||||
|     public static final String NAMED_COMMONS_CSRF = "commons-csrf"; |     public static final String NAMED_COMMONS_CSRF = "commons-csrf"; | ||||||
|  |     public static final String NAMED_WIKI_CSRF = "wiki-csrf"; | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|  | @ -73,6 +79,12 @@ public class NetworkingModule { | ||||||
|             .build(); |             .build(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) { | ||||||
|  |         return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public HttpLoggingInterceptor provideHttpLoggingInterceptor() { |     public HttpLoggingInterceptor provideHttpLoggingInterceptor() { | ||||||
|  | @ -88,29 +100,86 @@ public class NetworkingModule { | ||||||
|     public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, |     public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, | ||||||
|                                                           DepictsClient depictsClient, |                                                           DepictsClient depictsClient, | ||||||
|                                                           @Named("tools_forge") HttpUrl toolsForgeUrl, |                                                           @Named("tools_forge") HttpUrl toolsForgeUrl, | ||||||
|                                                           @Named("test_tools_forge") HttpUrl testToolsForgeUrl, |  | ||||||
|                                                           @Named("default_preferences") JsonKvStore defaultKvStore, |                                                           @Named("default_preferences") JsonKvStore defaultKvStore, | ||||||
|                                                           Gson gson) { |                                                           Gson gson) { | ||||||
|         return new OkHttpJsonApiClient(okHttpClient, |         return new OkHttpJsonApiClient(okHttpClient, | ||||||
|                 depictsClient, |                 depictsClient, | ||||||
|                 toolsForgeUrl, |                 toolsForgeUrl, | ||||||
|                 testToolsForgeUrl, |  | ||||||
|                 WIKIDATA_SPARQL_QUERY_URL, |                 WIKIDATA_SPARQL_QUERY_URL, | ||||||
|                 BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, |                 BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, | ||||||
|             gson); |             gson); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Named(NAMED_COMMONS_CSRF) |  | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |     public CommonsCookieStorage provideCookieStorage( | ||||||
|         return new CsrfTokenClient(commonsWikiSite, commonsWikiSite); |         @Named("default_preferences") JsonKvStore preferences) { | ||||||
|  |         CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences); | ||||||
|  |         cookieStorage.load(); | ||||||
|  |         return cookieStorage; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public LoginClient provideLoginClient() { |     public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) { | ||||||
|         return new LoginClient(); |         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 |     @Provides | ||||||
|  | @ -129,21 +198,6 @@ public class NetworkingModule { | ||||||
|         return HttpUrl.parse(TOOLS_FORGE_URL); |         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 |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     @Named(NAMED_WIKI_DATA_WIKI_SITE) |     @Named(NAMED_WIKI_DATA_WIKI_SITE) | ||||||
|  | @ -164,54 +218,40 @@ public class NetworkingModule { | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     @Named("commons-service") |     public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) { | ||||||
|     public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |         return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class); | ||||||
|         return ServiceFactory.get(commonsWikiSite); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     @Named("wikidata-service") |     public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) { | ||||||
|     public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { |         return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class); | ||||||
|         return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |     public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); |         return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { |     public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class); |         return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.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); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Named("commons-page-edit-service") |     @Named("commons-page-edit-service") | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |     public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class); |         return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Named("wikidata-page-edit-service") |     @Named("wikidata-page-edit-service") | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { |     public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class); |         return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Named("commons-page-edit") |     @Named("commons-page-edit") | ||||||
|  | @ -222,10 +262,25 @@ public class NetworkingModule { | ||||||
|         return new PageEditClient(csrfTokenClient, pageEditInterface); |         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 |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |     public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient, | ||||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class); |         @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 |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public WikidataMediaInterface provideWikidataMediaInterface( |     public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         @Named(NAMED_COMMONS_WIKI_SITE) final WikiSite commonsWikiSite) { |         return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class); | ||||||
|         return ServiceFactory.get(commonsWikiSite, |  | ||||||
|             BetaConstants.COMMONS_URL, WikidataMediaInterface.class); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) { |     public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class); |         return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public CategoryInterface provideCategoryInterface( |     public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         @Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |         return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class); | ||||||
|         return ServiceFactory |  | ||||||
|                .get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { |     public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UserInterface.class); |         return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public WikidataInterface provideWikidataInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { |     public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, WikidataInterface.class); |         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 |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite) { |     public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) { | ||||||
|         return ServiceFactory.get(wikiSite, wikiSite.url(), PageMediaInterface.class); |         return serviceFactory.create(wikiSite.url(), PageMediaInterface.class); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |     @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.annotation.Nullable; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.viewpager.widget.ViewPager.OnPageChangeListener; | import androidx.viewpager.widget.ViewPager.OnPageChangeListener; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import com.google.android.material.tabs.TabLayout; | import com.google.android.material.tabs.TabLayout; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.ViewPagerAdapter; | import fr.free.nrw.commons.ViewPagerAdapter; | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | 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 EXPLORE_MAP = "Map"; | ||||||
|     private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment"; |     private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment"; | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.tab_layout) | 
 | ||||||
|     TabLayout tabLayout; |     public FragmentExploreBinding binding; | ||||||
|     @BindView(R.id.viewPager) |  | ||||||
|     ParentViewPager viewPager; |  | ||||||
|     ViewPagerAdapter viewPagerAdapter; |     ViewPagerAdapter viewPagerAdapter; | ||||||
|     private ExploreListRootFragment featuredRootFragment; |     private ExploreListRootFragment featuredRootFragment; | ||||||
|     private ExploreListRootFragment mobileRootFragment; |     private ExploreListRootFragment mobileRootFragment; | ||||||
|  | @ -46,7 +43,10 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|     public JsonKvStore applicationKvStore; |     public JsonKvStore applicationKvStore; | ||||||
| 
 | 
 | ||||||
|     public void setScroll(boolean canScroll){ |     public void setScroll(boolean canScroll){ | ||||||
|         viewPager.setCanScroll(canScroll); |         if (binding != null) | ||||||
|  |         { | ||||||
|  |             binding.viewPager.setCanScroll(canScroll); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|  | @ -56,22 +56,17 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|         return fragment; |         return fragment; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | ||||||
|         @Nullable Bundle savedInstanceState) { |         @Nullable Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         View view = inflater.inflate(R.layout.fragment_explore, container, false); |         binding = FragmentExploreBinding.inflate(inflater, container, false); | ||||||
|         ButterKnife.bind(this, view); | 
 | ||||||
|         viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); |         viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); | ||||||
|         viewPager.setAdapter(viewPagerAdapter); |         binding.viewPager.setAdapter(viewPagerAdapter); | ||||||
|         viewPager.setId(R.id.viewPager); |         binding.viewPager.setId(R.id.viewPager); | ||||||
|         tabLayout.setupWithViewPager(viewPager); |         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||||
|         viewPager.addOnPageChangeListener(new OnPageChangeListener() { |         binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { | ||||||
|             @Override |             @Override | ||||||
|             public void onPageScrolled(int position, float positionOffset, |             public void onPageScrolled(int position, float positionOffset, | ||||||
|                 int positionOffsetPixels) { |                 int positionOffsetPixels) { | ||||||
|  | @ -81,9 +76,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|             @Override |             @Override | ||||||
|             public void onPageSelected(int position) { |             public void onPageSelected(int position) { | ||||||
|                 if (position == 2) { |                 if (position == 2) { | ||||||
|                     viewPager.setCanScroll(false); |                     binding.viewPager.setCanScroll(false); | ||||||
|                 } else { |                 } else { | ||||||
|                     viewPager.setCanScroll(true); |                     binding.viewPager.setCanScroll(true); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -94,7 +89,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|         }); |         }); | ||||||
|         setTabs(); |         setTabs(); | ||||||
|         setHasOptionsMenu(true); |         setHasOptionsMenu(true); | ||||||
|         return view; |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -133,13 +128,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean onBackPressed() { |     public boolean onBackPressed() { | ||||||
|         if (tabLayout.getSelectedTabPosition() == 0) { |         if (binding.tabLayout.getSelectedTabPosition() == 0) { | ||||||
|             if (featuredRootFragment.backPressed()) { |             if (featuredRootFragment.backPressed()) { | ||||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() |                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||||
|                     .setDisplayHomeAsUpEnabled(false); |                     .setDisplayHomeAsUpEnabled(false); | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|         } else if (tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment |         } else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment | ||||||
|             if (mobileRootFragment.backPressed()) { |             if (mobileRootFragment.backPressed()) { | ||||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() |                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||||
|                     .setDisplayHomeAsUpEnabled(false); |                     .setDisplayHomeAsUpEnabled(false); | ||||||
|  | @ -180,6 +175,12 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | ||||||
|                 return super.onOptionsItemSelected(item); |                 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.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; | import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||||
|  | @ -26,8 +25,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | ||||||
|     private MediaDetailPagerFragment mediaDetails; |     private MediaDetailPagerFragment mediaDetails; | ||||||
|     private CategoriesMediaFragment listFragment; |     private CategoriesMediaFragment listFragment; | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.explore_container) |     private FragmentFeaturedRootBinding binding; | ||||||
|     FrameLayout container; |  | ||||||
| 
 | 
 | ||||||
|     public ExploreListRootFragment() { |     public ExploreListRootFragment() { | ||||||
|         //empty constructor necessary otherwise crashes on recreate |         //empty constructor necessary otherwise crashes on recreate | ||||||
|  | @ -47,9 +45,9 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | ||||||
|         @Nullable final ViewGroup container, |         @Nullable final ViewGroup container, | ||||||
|         @Nullable final Bundle savedInstanceState) { |         @Nullable final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | 
 | ||||||
|         ButterKnife.bind(this, view); |         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||||
|         return view; |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -109,9 +107,13 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onMediaClicked(int position) { |     public void onMediaClicked(int position) { | ||||||
|         container.setVisibility(View.VISIBLE); |         if (binding!=null) { | ||||||
|         ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); |             binding.exploreContainer.setVisibility(View.VISIBLE); | ||||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); |         } | ||||||
|  |         if (((ExploreFragment) getParentFragment()).binding!=null) { | ||||||
|  |             ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|         ((ExploreFragment) getParentFragment()).setScroll(false); |         ((ExploreFragment) getParentFragment()).setScroll(false); | ||||||
|         setFragment(mediaDetails, listFragment); |         setFragment(mediaDetails, listFragment); | ||||||
|         mediaDetails.showImage(position); |         mediaDetails.showImage(position); | ||||||
|  | @ -185,16 +187,29 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem | ||||||
|      */ |      */ | ||||||
|     public boolean backPressed() { |     public boolean backPressed() { | ||||||
|         if (null != mediaDetails && mediaDetails.isVisible()) { |         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); |             removeFragment(mediaDetails); | ||||||
|             ((ExploreFragment) getParentFragment()).setScroll(true); |             ((ExploreFragment) getParentFragment()).setScroll(true); | ||||||
|             setFragment(listFragment, mediaDetails); |             setFragment(listFragment, mediaDetails); | ||||||
|             ((MainActivity) getActivity()).showTabs(); |             ((MainActivity) getActivity()).showTabs(); | ||||||
|             return true; |             return true; | ||||||
|         } else { |         } 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; |         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.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; | 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.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.explore.map.ExploreMapFragment; | import fr.free.nrw.commons.explore.map.ExploreMapFragment; | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||||
|  | @ -26,8 +25,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | ||||||
|     private MediaDetailPagerFragment mediaDetails; |     private MediaDetailPagerFragment mediaDetails; | ||||||
|     private ExploreMapFragment mapFragment; |     private ExploreMapFragment mapFragment; | ||||||
| 
 | 
 | ||||||
|     @BindView(R.id.explore_container) |     private FragmentFeaturedRootBinding binding; | ||||||
|     FrameLayout container; |  | ||||||
| 
 | 
 | ||||||
|     public ExploreMapRootFragment() { |     public ExploreMapRootFragment() { | ||||||
|         //empty constructor necessary otherwise crashes on recreate |         //empty constructor necessary otherwise crashes on recreate | ||||||
|  | @ -54,9 +52,10 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | ||||||
|         @Nullable final ViewGroup container, |         @Nullable final ViewGroup container, | ||||||
|         @Nullable final Bundle savedInstanceState) { |         @Nullable final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | 
 | ||||||
|         ButterKnife.bind(this, view); |         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); | ||||||
|         return view; | 
 | ||||||
|  |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -116,9 +115,9 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onMediaClicked(int position) { |     public void onMediaClicked(int position) { | ||||||
|         container.setVisibility(View.VISIBLE); |         binding.exploreContainer.setVisibility(View.VISIBLE); | ||||||
|         ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); |         ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); | ||||||
|         mediaDetails = new MediaDetailPagerFragment(false, true); |         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|         ((ExploreFragment) getParentFragment()).setScroll(false); |         ((ExploreFragment) getParentFragment()).setScroll(false); | ||||||
|         setFragment(mediaDetails, mapFragment); |         setFragment(mediaDetails, mapFragment); | ||||||
|         mediaDetails.showImage(position); |         mediaDetails.showImage(position); | ||||||
|  | @ -192,7 +191,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | ||||||
|      */ |      */ | ||||||
|     public boolean backPressed() { |     public boolean backPressed() { | ||||||
|         if (null != mediaDetails && mediaDetails.isVisible()) { |         if (null != mediaDetails && mediaDetails.isVisible()) { | ||||||
|             ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE); |             ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|             removeFragment(mediaDetails); |             removeFragment(mediaDetails); | ||||||
|             ((ExploreFragment) getParentFragment()).setScroll(true); |             ((ExploreFragment) getParentFragment()).setScroll(true); | ||||||
|             setFragment(mapFragment, mediaDetails); |             setFragment(mapFragment, mediaDetails); | ||||||
|  | @ -213,4 +212,11 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme | ||||||
|         ((MainActivity) getActivity()).showTabs(); |         ((MainActivity) getActivity()).showTabs(); | ||||||
|         return false; |         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.os.Bundle; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.FrameLayout; |  | ||||||
| import android.widget.SearchView; |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.appcompat.widget.Toolbar; |  | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import androidx.fragment.app.FragmentTransaction; | 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.view.RxView; | ||||||
| import com.jakewharton.rxbinding2.widget.RxSearchView; | import com.jakewharton.rxbinding2.widget.RxSearchView; | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.ViewPagerAdapter; | import fr.free.nrw.commons.ViewPagerAdapter; | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | 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.categories.search.SearchCategoryFragment; | ||||||
| import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; | import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; | ||||||
| import fr.free.nrw.commons.explore.media.SearchMediaFragment; | import fr.free.nrw.commons.explore.media.SearchMediaFragment; | ||||||
|  | @ -45,13 +39,6 @@ import timber.log.Timber; | ||||||
| public class SearchActivity extends BaseActivity | public class SearchActivity extends BaseActivity | ||||||
|         implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { |         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 |     @Inject | ||||||
|     RecentSearchesDao recentSearchesDao; |     RecentSearchesDao recentSearchesDao; | ||||||
| 
 | 
 | ||||||
|  | @ -63,25 +50,28 @@ public class SearchActivity extends BaseActivity | ||||||
|     private MediaDetailPagerFragment mediaDetails; |     private MediaDetailPagerFragment mediaDetails; | ||||||
|     ViewPagerAdapter viewPagerAdapter; |     ViewPagerAdapter viewPagerAdapter; | ||||||
| 
 | 
 | ||||||
|  |     private ActivitySearchBinding binding; | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         setContentView(R.layout.activity_search); |         binding = ActivitySearchBinding.inflate(getLayoutInflater()); | ||||||
|         ButterKnife.bind(this); |         setContentView(binding.getRoot()); | ||||||
|  | 
 | ||||||
|         setTitle(getString(R.string.title_activity_search)); |         setTitle(getString(R.string.title_activity_search)); | ||||||
|         setSupportActionBar(toolbar); |         setSupportActionBar(binding.toolbarSearch); | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|         toolbar.setNavigationOnClickListener(v->onBackPressed()); |         binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed()); | ||||||
|         supportFragmentManager = getSupportFragmentManager(); |         supportFragmentManager = getSupportFragmentManager(); | ||||||
|         setSearchHistoryFragment(); |         setSearchHistoryFragment(); | ||||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); |         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||||
|         viewPager.setAdapter(viewPagerAdapter); |         binding.viewPager.setAdapter(viewPagerAdapter); | ||||||
|         viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive |         binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive | ||||||
|         tabLayout.setupWithViewPager(viewPager); |         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||||
|         setTabs(); |         setTabs(); | ||||||
|         searchView.setQueryHint(getString(R.string.search_commons)); |         binding.searchBox.setQueryHint(getString(R.string.search_commons)); | ||||||
|         searchView.onActionViewExpanded(); |         binding.searchBox.onActionViewExpanded(); | ||||||
|         searchView.clearFocus(); |         binding.searchBox.clearFocus(); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -113,8 +103,8 @@ public class SearchActivity extends BaseActivity | ||||||
| 
 | 
 | ||||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); |         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||||
|         viewPagerAdapter.notifyDataSetChanged(); |         viewPagerAdapter.notifyDataSetChanged(); | ||||||
|         compositeDisposable.add(RxSearchView.queryTextChanges(searchView) |         compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox) | ||||||
|                 .takeUntil(RxView.detaches(searchView)) |                 .takeUntil(RxView.detaches(binding.searchBox)) | ||||||
|                 .debounce(500, TimeUnit.MILLISECONDS) |                 .debounce(500, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(this::handleSearch, Timber::e |                 .subscribe(this::handleSearch, Timber::e | ||||||
|  | @ -124,9 +114,9 @@ public class SearchActivity extends BaseActivity | ||||||
|     private void handleSearch(final CharSequence query) { |     private void handleSearch(final CharSequence query) { | ||||||
|         if (!TextUtils.isEmpty(query)) { |         if (!TextUtils.isEmpty(query)) { | ||||||
|             saveRecentSearch(query.toString()); |             saveRecentSearch(query.toString()); | ||||||
|             viewPager.setVisibility(View.VISIBLE); |             binding.viewPager.setVisibility(View.VISIBLE); | ||||||
|             tabLayout.setVisibility(View.VISIBLE); |             binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|             searchHistoryContainer.setVisibility(View.GONE); |             binding.searchHistoryContainer.setVisibility(View.GONE); | ||||||
| 
 | 
 | ||||||
|             if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { |             if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { | ||||||
|                 searchDepictionsFragment.onQueryUpdated(query.toString()); |                 searchDepictionsFragment.onQueryUpdated(query.toString()); | ||||||
|  | @ -144,10 +134,10 @@ public class SearchActivity extends BaseActivity | ||||||
|         else { |         else { | ||||||
|             //Open RecentSearchesFragment |             //Open RecentSearchesFragment | ||||||
|             recentSearchesFragment.updateRecentSearches(); |             recentSearchesFragment.updateRecentSearches(); | ||||||
|             viewPager.setVisibility(View.GONE); |             binding.viewPager.setVisibility(View.GONE); | ||||||
|             tabLayout.setVisibility(View.GONE); |             binding.tabLayout.setVisibility(View.GONE); | ||||||
|             setSearchHistoryFragment(); |             setSearchHistoryFragment(); | ||||||
|             searchHistoryContainer.setVisibility(View.VISIBLE); |             binding.searchHistoryContainer.setVisibility(View.VISIBLE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -215,13 +205,13 @@ public class SearchActivity extends BaseActivity | ||||||
|     @Override |     @Override | ||||||
|     public void onMediaClicked(int index) { |     public void onMediaClicked(int index) { | ||||||
|         ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); |         ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); | ||||||
|         tabLayout.setVisibility(View.GONE); |         binding.tabLayout.setVisibility(View.GONE); | ||||||
|         viewPager.setVisibility(View.GONE); |         binding.viewPager.setVisibility(View.GONE); | ||||||
|         mediaContainer.setVisibility(View.VISIBLE); |         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||||
|         searchView.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open |         binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open | ||||||
|         if (mediaDetails == null || !mediaDetails.isVisible()) { |         if (mediaDetails == null || !mediaDetails.isVisible()) { | ||||||
|             // set isFeaturedImage true for featured images, to include author field on media detail |             // set isFeaturedImage true for featured images, to include author field on media detail | ||||||
|             mediaDetails = new MediaDetailPagerFragment(false, true); |             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); | ||||||
|             supportFragmentManager |             supportFragmentManager | ||||||
|                     .beginTransaction() |                     .beginTransaction() | ||||||
|                     .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) |                     .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) | ||||||
|  | @ -269,12 +259,12 @@ public class SearchActivity extends BaseActivity | ||||||
|         } |         } | ||||||
|         if (getSupportFragmentManager().getBackStackEntryCount() == 1) { |         if (getSupportFragmentManager().getBackStackEntryCount() == 1) { | ||||||
|             // back to search so show search toolbar and hide navigation toolbar |             // back to search so show search toolbar and hide navigation toolbar | ||||||
|             searchView.setVisibility(View.VISIBLE);//set the searchview |             binding.searchBox.setVisibility(View.VISIBLE);//set the searchview | ||||||
|             tabLayout.setVisibility(View.VISIBLE); |             binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|             viewPager.setVisibility(View.VISIBLE); |             binding.viewPager.setVisibility(View.VISIBLE); | ||||||
|             mediaContainer.setVisibility(View.GONE); |             binding.mediaContainer.setVisibility(View.GONE); | ||||||
|         } else { |         } else { | ||||||
|             toolbar.setVisibility(View.GONE); |             binding.toolbarSearch.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|         super.onBackPressed(); |         super.onBackPressed(); | ||||||
|     } |     } | ||||||
|  | @ -284,15 +274,16 @@ public class SearchActivity extends BaseActivity | ||||||
|      * @param query Recent Search Query |      * @param query Recent Search Query | ||||||
|      */ |      */ | ||||||
|     public void updateText(String 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. |         // 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 |         // 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() { |     @Override protected void onDestroy() { | ||||||
|         super.onDestroy(); |         super.onDestroy(); | ||||||
|         //Dispose the disposables when the activity is destroyed |         //Dispose the disposables when the activity is destroyed | ||||||
|         compositeDisposable.dispose(); |         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.DepictedItem | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.get | import fr.free.nrw.commons.upload.structure.depictions.get | ||||||
| import fr.free.nrw.commons.wikidata.WikidataProperties | 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.DepictSearchItem | ||||||
|  | import fr.free.nrw.commons.wikidata.model.Entities | ||||||
|  | import fr.free.nrw.commons.wikidata.model.Statement_partial | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
| import org.wikipedia.wikidata.DataValue |  | ||||||
| import org.wikipedia.wikidata.Entities |  | ||||||
| import org.wikipedia.wikidata.Statement_partial |  | ||||||
| import java.util.* | import java.util.* | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Singleton | import javax.inject.Singleton | ||||||
|  |  | ||||||
|  | @ -13,8 +13,6 @@ import androidx.appcompat.widget.Toolbar; | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import androidx.viewpager.widget.ViewPager; | import androidx.viewpager.widget.ViewPager; | ||||||
| import butterknife.BindView; |  | ||||||
| import butterknife.ButterKnife; |  | ||||||
| import com.google.android.material.snackbar.Snackbar; | import com.google.android.material.snackbar.Snackbar; | ||||||
| import com.google.android.material.tabs.TabLayout; | import com.google.android.material.tabs.TabLayout; | ||||||
| import fr.free.nrw.commons.Media; | 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.ViewPagerAdapter; | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; | 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.child.ChildDepictionsFragment; | ||||||
| import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; | import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; | ||||||
| import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; | import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; | ||||||
|  | @ -57,14 +56,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | ||||||
|     @Inject |     @Inject | ||||||
|     DepictModel depictModel; |     DepictModel depictModel; | ||||||
|     private String wikidataItemName; |     private String wikidataItemName; | ||||||
|     @BindView(R.id.mediaContainer) |     private ActivityWikidataItemDetailsBinding binding; | ||||||
|     FrameLayout mediaContainer; |  | ||||||
|     @BindView(R.id.tab_layout) |  | ||||||
|     TabLayout tabLayout; |  | ||||||
|     @BindView(R.id.viewPager) |  | ||||||
|     ViewPager viewPager; |  | ||||||
|     @BindView(R.id.toolbar) |  | ||||||
|     Toolbar toolbar; |  | ||||||
| 
 | 
 | ||||||
|     ViewPagerAdapter viewPagerAdapter; |     ViewPagerAdapter viewPagerAdapter; | ||||||
|     private DepictedItem wikidataItem; |     private DepictedItem wikidataItem; | ||||||
|  | @ -72,19 +64,20 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         setContentView(R.layout.activity_wikidata_item_details); | 
 | ||||||
|         ButterKnife.bind(this); |         binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater()); | ||||||
|  |         setContentView(binding.getRoot()); | ||||||
|         compositeDisposable = new CompositeDisposable(); |         compositeDisposable = new CompositeDisposable(); | ||||||
|         supportFragmentManager = getSupportFragmentManager(); |         supportFragmentManager = getSupportFragmentManager(); | ||||||
|         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); |         viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); | ||||||
|         viewPager.setAdapter(viewPagerAdapter); |         binding.viewPager.setAdapter(viewPagerAdapter); | ||||||
|         viewPager.setOffscreenPageLimit(2); |         binding.viewPager.setOffscreenPageLimit(2); | ||||||
|         tabLayout.setupWithViewPager(viewPager); |         binding.tabLayout.setupWithViewPager(binding.viewPager); | ||||||
| 
 | 
 | ||||||
|         final DepictedItem depictedItem = getIntent().getParcelableExtra( |         final DepictedItem depictedItem = getIntent().getParcelableExtra( | ||||||
|             WikidataConstants.BOOKMARKS_ITEMS); |             WikidataConstants.BOOKMARKS_ITEMS); | ||||||
|         wikidataItem = depictedItem; |         wikidataItem = depictedItem; | ||||||
|         setSupportActionBar(toolbar); |         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|         setTabs(); |         setTabs(); | ||||||
|         setPageTitle(); |         setPageTitle(); | ||||||
|  | @ -137,7 +130,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | ||||||
|         fragmentList.add(parentDepictionsFragment); |         fragmentList.add(parentDepictionsFragment); | ||||||
|         titleList.add(getResources().getString(R.string.title_for_parent_classes)); |         titleList.add(getResources().getString(R.string.title_for_parent_classes)); | ||||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); |         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||||
|         viewPager.setOffscreenPageLimit(2); |         binding.viewPager.setOffscreenPageLimit(2); | ||||||
|         viewPagerAdapter.notifyDataSetChanged(); |         viewPagerAdapter.notifyDataSetChanged(); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  | @ -148,12 +141,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void onMediaClicked(int position) { |     public void onMediaClicked(int position) { | ||||||
|         tabLayout.setVisibility(View.GONE); |         binding.tabLayout.setVisibility(View.GONE); | ||||||
|         viewPager.setVisibility(View.GONE); |         binding.viewPager.setVisibility(View.GONE); | ||||||
|         mediaContainer.setVisibility(View.VISIBLE); |         binding.mediaContainer.setVisibility(View.VISIBLE); | ||||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { |         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { | ||||||
|             // set isFeaturedImage true for featured images, to include author field on media detail |             // 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(); |             FragmentManager supportFragmentManager = getSupportFragmentManager(); | ||||||
|             supportFragmentManager |             supportFragmentManager | ||||||
|                     .beginTransaction() |                     .beginTransaction() | ||||||
|  | @ -183,9 +176,9 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe | ||||||
|     @Override |     @Override | ||||||
|     public void onBackPressed() { |     public void onBackPressed() { | ||||||
|         if (supportFragmentManager.getBackStackEntryCount() == 1){ |         if (supportFragmentManager.getBackStackEntryCount() == 1){ | ||||||
|             tabLayout.setVisibility(View.VISIBLE); |             binding.tabLayout.setVisibility(View.VISIBLE); | ||||||
|             viewPager.setVisibility(View.VISIBLE); |             binding.viewPager.setVisibility(View.VISIBLE); | ||||||
|             mediaContainer.setVisibility(View.GONE); |             binding.mediaContainer.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|         super.onBackPressed(); |         super.onBackPressed(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -20,11 +20,11 @@ public class ExploreMapCalls { | ||||||
|     /** |     /** | ||||||
|      * Calls method to query Commons for uploads around a location |      * 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 |      * @return list of places obtained | ||||||
|      */ |      */ | ||||||
|     List<Media> callCommonsQuery(final LatLng curLatLng) { |     List<Media> callCommonsQuery(final LatLng currentLatLng) { | ||||||
|         String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude(); |         String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude(); | ||||||
|         return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); |         return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,45 +1,34 @@ | ||||||
| package fr.free.nrw.commons.explore.map; | package fr.free.nrw.commons.explore.map; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import com.mapbox.mapboxsdk.annotations.Marker; | import fr.free.nrw.commons.BaseMarker; | ||||||
| import com.mapbox.mapboxsdk.camera.CameraUpdate; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
| import fr.free.nrw.commons.location.LatLng; | import fr.free.nrw.commons.location.LatLng; | ||||||
| import fr.free.nrw.commons.location.LocationServiceManager; | import fr.free.nrw.commons.location.LocationServiceManager; | ||||||
| import fr.free.nrw.commons.nearby.NearbyBaseMarker; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
| public class ExploreMapContract { | public class ExploreMapContract { | ||||||
| 
 | 
 | ||||||
|     interface View { |     interface View { | ||||||
|         boolean isNetworkConnectionEstablished(); |         boolean isNetworkConnectionEstablished(); | ||||||
|         void populatePlaces(LatLng curlatLng,LatLng searchLatLng); |         void populatePlaces(LatLng curlatLng); | ||||||
|         void checkPermissionsAndPerformAction(); |         void askForLocationPermission(); | ||||||
|         void recenterMap(LatLng curLatLng); |         void recenterMap(LatLng curLatLng); | ||||||
|         void showLocationOffDialog(); |  | ||||||
|         void openLocationSettings(); |  | ||||||
|         void hideBottomDetailsSheet(); |         void hideBottomDetailsSheet(); | ||||||
|         void displayBottomSheetWithInfo(Marker marker); |         LatLng getMapCenter(); | ||||||
|         void addOnCameraMoveListener(); |         LatLng getMapFocus(); | ||||||
|  |         LatLng getLastMapFocus(); | ||||||
|  |         void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers); | ||||||
|  |         void clearAllMarkers(); | ||||||
|         void addSearchThisAreaButtonAction(); |         void addSearchThisAreaButtonAction(); | ||||||
|         void setSearchThisAreaButtonVisibility(boolean isVisible); |         void setSearchThisAreaButtonVisibility(boolean isVisible); | ||||||
|         void setProgressBarVisibility(boolean isVisible); |         void setProgressBarVisibility(boolean isVisible); | ||||||
|         boolean isDetailsBottomSheetVisible(); |         boolean isDetailsBottomSheetVisible(); | ||||||
|         boolean isSearchThisAreaButtonVisible(); |         boolean isSearchThisAreaButtonVisible(); | ||||||
|         void addCurrentLocationMarker(LatLng curLatLng); |  | ||||||
|         void updateMapToTrackPosition(LatLng curLatLng); |  | ||||||
|         Context getContext(); |         Context getContext(); | ||||||
|         LatLng getCameraTarget(); |  | ||||||
|         void centerMapToPlace(Place placeToCenter); |  | ||||||
|         LatLng getLastLocation(); |         LatLng getLastLocation(); | ||||||
|         com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation(); |  | ||||||
|         boolean isCurrentLocationMarkerVisible(); |  | ||||||
|         void setProjectorLatLngBounds(); |  | ||||||
|         void disableFABRecenter(); |         void disableFABRecenter(); | ||||||
|         void enableFABRecenter(); |         void enableFABRecenter(); | ||||||
|         void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker); |  | ||||||
|         void setMapBoundaries(CameraUpdate cameaUpdate); |  | ||||||
|         void setFABRecenterAction(android.view.View.OnClickListener onClickListener); |         void setFABRecenterAction(android.view.View.OnClickListener onClickListener); | ||||||
|         boolean backButtonClicked(); |         boolean backButtonClicked(); | ||||||
|     } |     } | ||||||
|  | @ -51,9 +40,6 @@ public class ExploreMapContract { | ||||||
|         void detachView(); |         void detachView(); | ||||||
|         void setActionListeners(JsonKvStore applicationKvStore); |         void setActionListeners(JsonKvStore applicationKvStore); | ||||||
|         boolean backButtonClicked(); |         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.RequestOptions; | ||||||
| import com.bumptech.glide.request.target.CustomTarget; | import com.bumptech.glide.request.target.CustomTarget; | ||||||
| import com.bumptech.glide.request.transition.Transition; | import com.bumptech.glide.request.transition.Transition; | ||||||
| import com.mapbox.mapboxsdk.annotations.IconFactory; | import fr.free.nrw.commons.BaseMarker; | ||||||
| import com.mapbox.mapboxsdk.annotations.Marker; |  | ||||||
| import fr.free.nrw.commons.MapController; | import fr.free.nrw.commons.MapController; | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.location.LatLng; | 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.nearby.Place; | ||||||
| import fr.free.nrw.commons.utils.ImageUtils; | import fr.free.nrw.commons.utils.ImageUtils; | ||||||
| import fr.free.nrw.commons.utils.LocationUtils; | import fr.free.nrw.commons.utils.LocationUtils; | ||||||
|  | @ -33,6 +31,7 @@ import javax.inject.Inject; | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| public class ExploreMapController extends MapController { | public class ExploreMapController extends MapController { | ||||||
|  | 
 | ||||||
|     private final ExploreMapCalls exploreMapCalls; |     private final ExploreMapCalls exploreMapCalls; | ||||||
|     public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used |     public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used | ||||||
|     public LatLng currentLocation; // current location of user |     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 |      * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, | ||||||
|      * @param curLatLng is current geolocation |      * explorePlaceList and boundaryCoordinates | ||||||
|      * @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 |      * @param currentLatLng                     is current geolocation | ||||||
|      * @return explorePlacesInfo info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates |      * @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) { |         if (searchLatLng == null) { | ||||||
|             Timber.d("Loading attractions explore map, but search is null"); |             Timber.d("Loading attractions explore map, but search is null"); | ||||||
|  | @ -61,7 +65,7 @@ public class ExploreMapController extends MapController { | ||||||
| 
 | 
 | ||||||
|         ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo(); |         ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo(); | ||||||
|         try { |         try { | ||||||
|             explorePlacesInfo.curLatLng = curLatLng; |             explorePlacesInfo.currentLatLng = currentLatLng; | ||||||
|             latestSearchLocation = searchLatLng; |             latestSearchLocation = searchLatLng; | ||||||
| 
 | 
 | ||||||
|             List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng); |             List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng); | ||||||
|  | @ -74,18 +78,23 @@ public class ExploreMapController extends MapController { | ||||||
|                 Timber.d("Sorting places by distance..."); |                 Timber.d("Sorting places by distance..."); | ||||||
|                 final Map<Media, Double> distances = new HashMap<>(); |                 final Map<Media, Double> distances = new HashMap<>(); | ||||||
|                 for (Media media : mediaList) { |                 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 |                     // 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(); |                         boundaryCoordinates[0] = media.getCoordinates(); | ||||||
|                     } |                     } | ||||||
|                     if (media.getCoordinates().getLatitude() > boundaryCoordinates[1].getLatitude()) { |                     if (media.getCoordinates().getLatitude() | ||||||
|  |                         > boundaryCoordinates[1].getLatitude()) { | ||||||
|                         boundaryCoordinates[1] = media.getCoordinates(); |                         boundaryCoordinates[1] = media.getCoordinates(); | ||||||
|                     } |                     } | ||||||
|                     if (media.getCoordinates().getLongitude() < boundaryCoordinates[2].getLongitude()) { |                     if (media.getCoordinates().getLongitude() | ||||||
|  |                         < boundaryCoordinates[2].getLongitude()) { | ||||||
|                         boundaryCoordinates[2] = media.getCoordinates(); |                         boundaryCoordinates[2] = media.getCoordinates(); | ||||||
|                     } |                     } | ||||||
|                     if (media.getCoordinates().getLongitude() > boundaryCoordinates[3].getLongitude()) { |                     if (media.getCoordinates().getLongitude() | ||||||
|  |                         > boundaryCoordinates[3].getLongitude()) { | ||||||
|                         boundaryCoordinates[3] = media.getCoordinates(); |                         boundaryCoordinates[3] = media.getCoordinates(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -96,7 +105,8 @@ public class ExploreMapController extends MapController { | ||||||
| 
 | 
 | ||||||
|             // Sets latestSearchRadius to maximum distance among boundaries and search location |             // Sets latestSearchRadius to maximum distance among boundaries and search location | ||||||
|             for (LatLng bound : boundaryCoordinates) { |             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) { |                 if (distance > latestSearchRadius) { | ||||||
|                     latestSearchRadius = distance; |                     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 |             // Our radius searched around us, will be used to understand when user search their own location, we will follow them | ||||||
|             if (checkingAroundCurrentLocation) { |             if (checkingAroundCurrentLocation) { | ||||||
|                 currentLocationSearchRadius = latestSearchRadius; |                 currentLocationSearchRadius = latestSearchRadius; | ||||||
|                 currentLocation = curLatLng; |                 currentLocation = currentLatLng; | ||||||
|             } |             } | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             e.printStackTrace(); |             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 |      * 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 |      * @return baseMarkerOptions list that holds nearby places with their icons | ||||||
|      */ |      */ | ||||||
|     public static List<NearbyBaseMarker> loadAttractionsFromLocationToBaseMarkerOptions( |     public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions( | ||||||
|         LatLng curLatLng, |         LatLng currentLatLng, | ||||||
|         final List<Place> placeList, |         final List<Place> placeList, | ||||||
|         Context context, |         Context context, | ||||||
|         NearbyBaseMarkerThumbCallback callback, |         NearbyBaseMarkerThumbCallback callback, | ||||||
|         Marker selectedMarker, |  | ||||||
|         boolean shouldTrackPosition, |  | ||||||
|         ExplorePlacesInfo explorePlacesInfo) { |         ExplorePlacesInfo explorePlacesInfo) { | ||||||
|         List<NearbyBaseMarker> baseMarkerOptions = new ArrayList<>(); |         List<BaseMarker> baseMarkerList = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|         if (placeList == null) { |         if (placeList == null) { | ||||||
|             return baseMarkerOptions; |             return baseMarkerList; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         VectorDrawableCompat vectorDrawable = null; |         VectorDrawableCompat vectorDrawable = null; | ||||||
|         try { |         try { | ||||||
|             vectorDrawable = VectorDrawableCompat.create( |             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) { |         } catch (Resources.NotFoundException e) { | ||||||
|             // ignore when running tests. |             // ignore when running tests. | ||||||
|         } |         } | ||||||
|         if (vectorDrawable != null) { |         if (vectorDrawable != null) { | ||||||
|             for (Place explorePlace : placeList) { |             for (Place explorePlace : placeList) { | ||||||
|                 final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); |                 final BaseMarker baseMarker = new BaseMarker(); | ||||||
|                 String distance = formatDistanceBetween(curLatLng, explorePlace.location); |                 String distance = formatDistanceBetween(currentLatLng, explorePlace.location); | ||||||
|                 explorePlace.setDistance(distance); |                 explorePlace.setDistance(distance); | ||||||
| 
 | 
 | ||||||
|                 nearbyBaseMarker.title(explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); |                 baseMarker.setTitle( | ||||||
|                 nearbyBaseMarker.position( |                     explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); | ||||||
|                     new com.mapbox.mapboxsdk.geometry.LatLng( |                 baseMarker.setPosition( | ||||||
|  |                     new fr.free.nrw.commons.location.LatLng( | ||||||
|                         explorePlace.location.getLatitude(), |                         explorePlace.location.getLatitude(), | ||||||
|                         explorePlace.location.getLongitude())); |                         explorePlace.location.getLongitude(), 0)); | ||||||
|                 nearbyBaseMarker.place(explorePlace); |                 baseMarker.setPlace(explorePlace); | ||||||
| 
 | 
 | ||||||
|                 Glide.with(context) |                 Glide.with(context) | ||||||
|                     .asBitmap() |                     .asBitmap() | ||||||
|  | @ -160,12 +170,15 @@ public class ExploreMapController extends MapController { | ||||||
|                     .into(new CustomTarget<Bitmap>() { |                     .into(new CustomTarget<Bitmap>() { | ||||||
|                         // We add icons to markers when bitmaps are ready |                         // We add icons to markers when bitmaps are ready | ||||||
|                         @Override |                         @Override | ||||||
|                         public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { |                         public void onResourceReady(@NonNull Bitmap resource, | ||||||
|                             nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromBitmap( |                             @Nullable Transition<? super Bitmap> transition) { | ||||||
|                                 ImageUtils.addRedBorder(resource, 6, context))); |                             baseMarker.setIcon( | ||||||
|                             baseMarkerOptions.add(nearbyBaseMarker); |                                 ImageUtils.addRedBorder(resource, 6, context)); | ||||||
|                             if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback |                             baseMarkerList.add(baseMarker); | ||||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition); |                             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 |                         @Override | ||||||
|                         public void onLoadFailed(@Nullable final Drawable errorDrawable) { |                         public void onLoadFailed(@Nullable final Drawable errorDrawable) { | ||||||
|                             super.onLoadFailed(errorDrawable); |                             super.onLoadFailed(errorDrawable); | ||||||
|                             nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromResource(R.drawable.image_placeholder_96)); |                             baseMarker.fromResource(context, R.drawable.image_placeholder_96); | ||||||
|                             baseMarkerOptions.add(nearbyBaseMarker); |                             baseMarkerList.add(baseMarker); | ||||||
|                             if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback |                             if (baseMarkerList.size() | ||||||
|                                 callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition); |                                 == 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 { |     interface NearbyBaseMarkerThumbCallback { | ||||||
|  | 
 | ||||||
|         // Callback to notify thumbnails of explore markers are added as icons and ready |         // 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