Merge branch 'main' into replace_toasts

This commit is contained in:
Priyank Shankar 2024-08-21 19:49:52 +05:30 committed by GitHub
commit 759ed34cf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
922 changed files with 29317 additions and 44373 deletions

View file

@ -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
View file

@ -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

View file

@ -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
View 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>

View file

@ -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

View file

@ -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

View file

@ -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).

View file

@ -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
}

View file

@ -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 ---

View file

@ -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));
}
}

View file

@ -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(

View file

@ -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>

View 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
}
}
}

View 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)
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
} }

View file

@ -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

View file

@ -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();
} }
} }

View file

@ -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() {
} }

View file

@ -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;

View file

@ -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
} }

View file

@ -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");
}

View file

@ -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

View file

@ -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() }
) )

View file

@ -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;
}
}
} }

View file

@ -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))

View file

@ -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;

View file

@ -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
}
}

View file

@ -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()
} }
} }
} }

View file

@ -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&section=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?>

View file

@ -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)
}
} }
} }
} }

View file

@ -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?>
}

View file

@ -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();
} }

View file

@ -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())));
}
}

View file

@ -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;
}); }
)
);
} }
/** /**

View file

@ -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);
}
} }

View file

@ -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)

View file

@ -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?>
}

View file

@ -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()
}

View file

@ -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)
}

View 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
}
}
}

View file

@ -0,0 +1,3 @@
package fr.free.nrw.commons.auth.login
class LoginFailedException(message: String?) : Throwable(message)

View file

@ -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?>
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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 {

View file

@ -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;
}
} }

View file

@ -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 {
} }
} }
} }
} }

View file

@ -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;
}
} }

View file

@ -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();
} }
} }

View file

@ -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) {

View file

@ -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()
} }
/** /**

View file

@ -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

View file

@ -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();
} }

View file

@ -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;

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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?,

View file

@ -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
} }
} }

View file

@ -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;
} }
} }

View file

@ -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

View file

@ -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() {

View file

@ -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.
}
} }

View file

@ -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

View file

@ -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);
}, () -> { }, () -> {

View file

@ -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));
}));
} }
} }

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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
} }

View file

@ -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
} }
} }

View file

@ -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"
} }
} }

View file

@ -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.
} }
} }

View file

@ -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).
* *
*/ */
} }

View file

@ -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()
}
}

View file

@ -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
} }
} }

View file

@ -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

View file

@ -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);
} }

View file

@ -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();

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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();
} }

View file

@ -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

View file

@ -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.
*/ */

View file

@ -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

View 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
}
}

View 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)
}
}

View 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?
}

View 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
}
}

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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;
} }
} }

View file

@ -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

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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);
} }
} }

View file

@ -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);
} }
} }

Some files were not shown because too many files have changed in this diff Show more