mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-11-02 15:53:55 +01:00
Merge branch 'main' into replace_toasts
This commit is contained in:
commit
759ed34cf2
922 changed files with 29317 additions and 44373 deletions
18
.github/workflows/android.yml
vendored
18
.github/workflows/android.yml
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
name: Android CI
|
name: Android CI
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
|
@ -12,17 +12,17 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2.4.0
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v2.5.0
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: 'temurin'
|
||||||
java-version: 11
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache packages
|
- name: Cache packages
|
||||||
id: cache-packages
|
id: cache-packages
|
||||||
uses: actions/cache@v2.1.7
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
|
|
||||||
- name: AVD cache
|
- name: AVD cache
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
id: avd-cache
|
id: avd-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -89,7 +89,7 @@ jobs:
|
||||||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload betaDebug APK
|
- name: Upload betaDebug APK
|
||||||
uses: actions/upload-artifact@v2.3.1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: betaDebugAPK
|
name: betaDebugAPK
|
||||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||||
|
|
@ -98,7 +98,7 @@ jobs:
|
||||||
run: bash ./gradlew assembleProdDebug --stacktrace
|
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload prodDebug APK
|
- name: Upload prodDebug APK
|
||||||
uses: actions/upload-artifact@v2.3.1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: prodDebugAPK
|
name: prodDebugAPK
|
||||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -43,3 +43,7 @@ app/src/main/jniLibs
|
||||||
#https://docs.opencv.org/3.3.0/
|
#https://docs.opencv.org/3.3.0/
|
||||||
/libraries/opencv/javadoc/
|
/libraries/opencv/javadoc/
|
||||||
captures/*
|
captures/*
|
||||||
|
|
||||||
|
# Test and other output
|
||||||
|
app/jacoco.exec
|
||||||
|
app/CommonsContributions
|
||||||
17
.idea/codeStyles/Project.xml
generated
17
.idea/codeStyles/Project.xml
generated
|
|
@ -39,21 +39,18 @@
|
||||||
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
|
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
|
||||||
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
|
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
|
||||||
</Objective-C>
|
</Objective-C>
|
||||||
<Objective-C-extensions>
|
|
||||||
<extensions>
|
|
||||||
<pair source="cc" header="h" fileNamingConvention="NONE" />
|
|
||||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
|
||||||
</extensions>
|
|
||||||
</Objective-C-extensions>
|
|
||||||
<Python>
|
<Python>
|
||||||
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
|
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
|
||||||
</Python>
|
</Python>
|
||||||
<TypeScriptCodeStyleSettings>
|
<TypeScriptCodeStyleSettings>
|
||||||
<option name="INDENT_CHAINED_CALLS" value="false" />
|
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||||
</TypeScriptCodeStyleSettings>
|
</TypeScriptCodeStyleSettings>
|
||||||
<XML>
|
<files>
|
||||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
<extensions>
|
||||||
</XML>
|
<pair source="cc" header="h" fileNamingConvention="NONE" />
|
||||||
|
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||||
|
</extensions>
|
||||||
|
</files>
|
||||||
<codeStyleSettings language="CSS">
|
<codeStyleSettings language="CSS">
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
|
@ -318,9 +315,7 @@
|
||||||
<codeStyleSettings language="protobuf">
|
<codeStyleSettings language="protobuf">
|
||||||
<option name="RIGHT_MARGIN" value="80" />
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
<option name="TAB_SIZE" value="2" />
|
|
||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
|
|
|
||||||
5
.mailmap
Normal file
5
.mailmap
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# See: https://git-scm.com/docs/git-shortlog#_mapping_authors
|
||||||
|
#
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org>
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org> <brion@wikimedia.org>
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org> <brion@pobox.com>
|
||||||
149
CHANGELOG.md
149
CHANGELOG.md
|
|
@ -1,5 +1,154 @@
|
||||||
# Wikimedia Commons for Android
|
# Wikimedia Commons for Android
|
||||||
|
|
||||||
|
## v5.0.2
|
||||||
|
|
||||||
|
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
|
||||||
|
same category and depictions.
|
||||||
|
- Show Wikidata description on currently active Nearby pin to provide more useful information.
|
||||||
|
- Improve the visibility of map markers by dynamically adjusting their colors based on the app's
|
||||||
|
theme. The map markers will now appear lighter when the app is in dark mode and darker when the
|
||||||
|
app is in light mode. This change aims to enhance marker visibility and improve the overall user
|
||||||
|
experience.
|
||||||
|
- Added information on where user feedback is posted, helping users track existing feedback and
|
||||||
|
monitor their own submissions.
|
||||||
|
- Enhanced the edit location screen of the upload screen by centering the map on the picture's
|
||||||
|
location from metadata when editing, or on the device's GPS location if metadata is unavailable,
|
||||||
|
improving accuracy and user experience.
|
||||||
|
- Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a
|
||||||
|
recently uploaded image, enhancing clarity and user experience.
|
||||||
|
- Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user
|
||||||
|
experience by showing loading progress until the image is fully loaded.
|
||||||
|
- Fixed an issue where caption and description fields would intermittently disappear when using
|
||||||
|
voice input, ensuring text remains visible and stable across all entries.
|
||||||
|
- Fixed a crash that occurred when attempting to remove multiple instances of caption/description
|
||||||
|
fields after initially adding them.
|
||||||
|
- Improve the text in the prompt shown when skipping login to sound more natural.
|
||||||
|
- Modified feedback addition logic to append new sections at the bottom of the page, ensuring
|
||||||
|
auto-archiving of sections functions correctly on the feedback page.
|
||||||
|
- Resolved issue where the app failed to clear cookies upon logout.
|
||||||
|
|
||||||
|
## v5.0.1
|
||||||
|
|
||||||
|
Same as v5.0.0 except this fixes some R8 rules to ensure that the release
|
||||||
|
variants of the app work as intended.
|
||||||
|
|
||||||
|
## v5.0.0
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
|
||||||
|
- Redesigned the map feature to **replace Mapbox with the osmdroid library**.
|
||||||
|
Key elements like pin visualization and user-centered display are still
|
||||||
|
included in this redesign. This is done to guard against possible misuse of
|
||||||
|
the Mapbox token and, more crucially, to keep the app from becoming dependent
|
||||||
|
on a service that charges for usage but offers a free tier.
|
||||||
|
|
||||||
|
With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org).
|
||||||
|
- Add the ability to **export locations of nearby missing pictures in GPX and
|
||||||
|
KML formats**. This allows users to browse the locations with desired radius
|
||||||
|
for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing
|
||||||
|
accessibility and offline functionality.
|
||||||
|
- **Limited the uploads via the custom image picker** to a maximum of 20.
|
||||||
|
- Added two menu choices for **transparent image backgrounds**, giving users the
|
||||||
|
option of either a black or white background, increasing adaptability to
|
||||||
|
various theme settings.
|
||||||
|
|
||||||
|
User customization option has been provided with the
|
||||||
|
ability to save background color selections permanently on a per image basis.
|
||||||
|
- Implemented functionality to **automatically resume uploads** that become
|
||||||
|
stuck due to app termination or device reboot.
|
||||||
|
- Added a **compass arrow in the Nearby banner** shown in the "Contributions"
|
||||||
|
screen to guide users towards the nearest item, thus providing the missing
|
||||||
|
directional cues. The arrow dynamically adjusts based on device rotation,
|
||||||
|
aligning with the calculated bearing towards the target location. Further,
|
||||||
|
the distance and direction are updated as the user moves.
|
||||||
|
- Implemented **voice input feature** for caption and description fields,
|
||||||
|
enabling users to dictate text directly into these fields.
|
||||||
|
- Improved various flows in the app to **redirect users to the login page** and
|
||||||
|
display a persistent message **if their session becomes invalid** due to a
|
||||||
|
password change, enhancing user guidance and security measures.
|
||||||
|
|
||||||
|
### Revamps and refactorings
|
||||||
|
|
||||||
|
- **Revamped initial upload screen layout and the description edit screen layout**
|
||||||
|
for enhanced user experience and ensuring better symmetry in the design.
|
||||||
|
- **Replaced Butterknife with ViewBinding** in various places of the app.
|
||||||
|
- Transferred essential code from **the redundant data-client module** to the
|
||||||
|
main Commons app code, enabling its integration and facilitating the removal
|
||||||
|
of the redundant module. Further, convert various parts of the code to Kotlin.
|
||||||
|
- **Revamped the various location permission flows** to ensure consistency for
|
||||||
|
the sake of a better user experience.
|
||||||
|
|
||||||
|
### Bug fixes and various changes
|
||||||
|
|
||||||
|
- Resolved an issue where paused uploads that were subsequently cancelled were
|
||||||
|
still being uploaded.
|
||||||
|
- Fixed an issue where some user information such as upload count were not
|
||||||
|
displayed in the "Contributions" and "Profile" screens.
|
||||||
|
- Fixed the long-standing broken *"Picture of the Day" widget* to restore its
|
||||||
|
usability.
|
||||||
|
- Resolved an issue where some categories were hidden at the top of Upload
|
||||||
|
Wizard suggestions.
|
||||||
|
- Resolved an issue where there was a grey empty screen at Upload wizard when
|
||||||
|
the app was denied the files permission.
|
||||||
|
- Implemented logic to bypass media in Peer Review if the current reviewer is
|
||||||
|
also the user who uploaded the media.
|
||||||
|
- Corrected arrow image behaviour in the first upload screen: now displays down
|
||||||
|
arrow when details card is fully visible, aligning with expected user
|
||||||
|
interaction.
|
||||||
|
- Updated app icon to improve visibility and recognition on F-Droid.
|
||||||
|
- Fixed issue causing all pictures to disappear and activity to reload fully in
|
||||||
|
the custom image selector after marking a picture as 'not for upload', now
|
||||||
|
ensuring only the selected picture is removed as expected.
|
||||||
|
|
||||||
|
What's listed here is only a subset of all the changes. Check the full-list of
|
||||||
|
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0).
|
||||||
|
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0)
|
||||||
|
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||||
|
|
||||||
|
## v4.2.1
|
||||||
|
|
||||||
|
- Provide the ability to edit an image to losslessly rotate it while uploading
|
||||||
|
- Fix a bug in v4.2.0 where the nearby places were not loading
|
||||||
|
- Fix a bug where editing depictions was showing a progress bar indefinitely
|
||||||
|
- In the upload screen, use different map icons to indicate if image is being uploaded with location
|
||||||
|
metadata
|
||||||
|
- For nearby uploads, it is no longer possible to deselect the item's category and depiction
|
||||||
|
- The Mapbox account key used by the app has been changed
|
||||||
|
- Category search now shows exact matches without any discrepancies
|
||||||
|
- Various bug and crash fixes
|
||||||
|
|
||||||
|
## v4.2.0
|
||||||
|
- Dark mode colour improvements
|
||||||
|
- Enhancements done to address location metadata loss including the metadata loss that occurs in
|
||||||
|
latest Android versions
|
||||||
|
- Enhancements done to address the issue where uploads get stuck in queued state
|
||||||
|
- Fix the inability to upload via the in-app camera option
|
||||||
|
- Provide the ability to optionally include location metadata for in-app camera uploads in case the
|
||||||
|
device camera app does not provide location metadata
|
||||||
|
- Use geo location URL that works consistently across all map applications
|
||||||
|
- Fix crash when clicking on location target icon while trying to edit the location of an upload
|
||||||
|
- Fix crash that occurs randomly while returning to the app after leaving it in the background
|
||||||
|
- Fix crash in Sign up activity on Android version 5.0 and 5.1
|
||||||
|
- Android 13 compatibility changes
|
||||||
|
|
||||||
|
## v4.1.0
|
||||||
|
- Location of pictures uploaded via custom picture selector are now recognized
|
||||||
|
- Improvements to the custom picture selector
|
||||||
|
- Ensure the WLM pictures are associated with the correct templates for each year
|
||||||
|
- Only show pictures uploaded via app in peer review
|
||||||
|
- Improve the variety of images show in peer review
|
||||||
|
- Allow going to current location in location edit dialog while uploading a picture
|
||||||
|
- Switch to using MapLibre instead of Mapbox and thereby disable telemetry sent to Mapbox
|
||||||
|
- Fixed various bugs
|
||||||
|
|
||||||
|
## v4.0.5
|
||||||
|
- Bumped min SDK to 29 to try and solve Google policy issue
|
||||||
|
- Reverted dialog
|
||||||
|
- Note: This encompasses versions 1031, 1032, and 1033, due to the Play Store's requirements to overwrite all the tracks with a post-fix version (otherwise no single track can be published)
|
||||||
|
|
||||||
|
## v4.0.4
|
||||||
|
- Added dialog for Google's location policy
|
||||||
|
|
||||||
## v4.0.3
|
## v4.0.3
|
||||||
- Added "Report" button for Google UGC policy
|
- Added "Report" button for Google UGC policy
|
||||||
|
|
||||||
|
|
|
||||||
1
CREDITS
1
CREDITS
|
|
@ -53,7 +53,6 @@ their contribution to the product.
|
||||||
* Butterknife
|
* Butterknife
|
||||||
* GSON
|
* GSON
|
||||||
* Timber
|
* Timber
|
||||||
* MapBox
|
|
||||||
|
|
||||||
3rd party open source apps from which significant code has been reused:
|
3rd party open source apps from which significant code has been reused:
|
||||||
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
||||||
|
|
||||||
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
|
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request! :-)
|
||||||
|
|
||||||
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
||||||
<img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a>
|
<img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a>
|
||||||
|
|
@ -15,7 +15,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
We try to have an extensive documentation at our [documentation repository][4]:
|
Our [documentation repository][4] contains extensive documentation for users, contributors, and developers alike:
|
||||||
|
|
||||||
* [User Documentation][5]
|
* [User Documentation][5]
|
||||||
* [Contributor Documentation][6]
|
* [Contributor Documentation][6]
|
||||||
|
|
@ -29,11 +29,11 @@ Thank you all for your work!
|
||||||
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
|
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
|
||||||
| :---: | :---: | :---: | :---: | :---: |
|
| :---: | :---: | :---: | :---: | :---: |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
|
| [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) |
|
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) |
|
| [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) |
|
| [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/54016427?v=4" width="100px;"/><br /><sub><b>4D17Y4</b></sub>](https://github.com/4D17Y4) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) |
|
||||||
|
|
||||||
|
|
||||||
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
||||||
|
|
|
||||||
115
app/build.gradle
115
app/build.gradle
|
|
@ -5,7 +5,7 @@ apply from: '../gitutils.gradle'
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply from: "$rootDir/jacoco.gradle"
|
apply from: "$rootDir/jacoco.gradle"
|
||||||
|
|
||||||
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
||||||
|
|
@ -16,13 +16,17 @@ if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation project(':wikimedia-data-client')
|
|
||||||
// Utils
|
// Utils
|
||||||
implementation 'in.yuvi:http.fluent:1.3'
|
implementation 'in.yuvi:http.fluent:1.3'
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
implementation 'com.google.code.gson:gson:2.8.5'
|
||||||
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"){
|
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){
|
||||||
force = true //API 19 support
|
// Forcing dependency versions using force = true on a first-level dependency has been deprecated.
|
||||||
|
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
|
||||||
|
//force = true //API 19 support
|
||||||
}
|
}
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||||
|
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
|
||||||
|
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
|
||||||
implementation 'com.squareup.okio:okio:2.2.2'
|
implementation 'com.squareup.okio:okio:2.2.2'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
|
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
|
||||||
|
|
@ -45,10 +49,8 @@ dependencies {
|
||||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||||
implementation 'com.karumi:dexter:5.0.0'
|
implementation 'com.karumi:dexter:5.0.0'
|
||||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
|
||||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
||||||
|
|
@ -73,30 +75,33 @@ dependencies {
|
||||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||||
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||||
|
|
||||||
//Mocking
|
//Mocking
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
testImplementation 'org.mockito:mockito-core:2.25.1'
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
|
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
||||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"
|
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
||||||
|
|
||||||
// Unit testing
|
// Unit testing
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||||
testImplementation 'androidx.test:core:1.4.0'
|
testImplementation 'androidx.test:core:1.5.0'
|
||||||
|
testImplementation "androidx.test:runner:1.5.2"
|
||||||
|
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
testImplementation "androidx.test:rules:1.5.0"
|
||||||
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
|
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
|
||||||
testImplementation "com.jraska.livedata:testing-ktx:1.1.2"
|
testImplementation "com.jraska.livedata:testing-ktx:1.1.2"
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
|
||||||
testImplementation 'com.facebook.soloader:soloader:0.10.1'
|
testImplementation 'com.facebook.soloader:soloader:0.10.5'
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
|
||||||
|
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
|
||||||
|
testImplementation "commons-io:commons-io:2.6"
|
||||||
|
|
||||||
// Android testing
|
// Android testing
|
||||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04'
|
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04'
|
||||||
|
|
@ -119,8 +124,7 @@ dependencies {
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
||||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
||||||
implementation "androidx.multidex:multidex:2.0.1"
|
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||||
compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
|
||||||
|
|
||||||
//swipe_layout
|
//swipe_layout
|
||||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
||||||
|
|
@ -131,7 +135,6 @@ dependencies {
|
||||||
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
|
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
|
||||||
kapt "androidx.room:room-compiler:$ROOM_VERSION"
|
kapt "androidx.room:room-compiler:$ROOM_VERSION"
|
||||||
// For Kotlin use kapt instead of annotationProcessor
|
// For Kotlin use kapt instead of annotationProcessor
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
|
|
||||||
// Pref
|
// Pref
|
||||||
|
|
@ -139,10 +142,12 @@ dependencies {
|
||||||
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
||||||
|
//Android Media
|
||||||
|
implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
|
||||||
|
|
||||||
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
||||||
|
|
||||||
def work_version = "2.8.0"
|
def work_version = "2.8.1"
|
||||||
// Kotlin + coroutines
|
// Kotlin + coroutines
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
implementation("androidx.work:work-runtime:$work_version")
|
implementation("androidx.work:work-runtime:$work_version")
|
||||||
|
|
@ -151,8 +156,21 @@ dependencies {
|
||||||
//Glide
|
//Glide
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||||
|
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||||
|
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||||
|
|
||||||
implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }
|
implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }
|
||||||
|
|
||||||
|
//OSMDroid
|
||||||
|
implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION")
|
||||||
|
constraints {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
|
||||||
|
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||||
|
}
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
|
||||||
|
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task disableAnimations(type: Exec) {
|
task disableAnimations(type: Exec) {
|
||||||
|
|
@ -168,17 +186,17 @@ project.gradle.taskGraph.whenReady {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
//applicationId 'fr.free.nrw.commons'
|
//applicationId 'fr.free.nrw.commons'
|
||||||
|
|
||||||
versionCode 1029
|
versionCode 1040
|
||||||
versionName '4.0.3'
|
versionName '5.0.2'
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 31
|
targetSdkVersion 33
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
|
|
@ -188,17 +206,23 @@ android {
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'META-INF/androidx.*'
|
jniLibs {
|
||||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
excludes += ['META-INF/androidx.*']
|
||||||
}
|
}
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
animationsDisabled true
|
animationsDisabled true
|
||||||
|
|
||||||
unitTests.returnDefaultValues = true
|
unitTests {
|
||||||
unitTests.includeAndroidResources = true
|
returnDefaultValues = true
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
unitTests.all {
|
unitTests.all {
|
||||||
jvmArgs '-noverify'
|
jvmArgs '-noverify'
|
||||||
|
|
@ -223,13 +247,14 @@ android {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
testProguardFile 'test-proguard-rules.txt'
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
|
||||||
testCoverageEnabled true
|
testCoverageEnabled true
|
||||||
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
testProguardFile 'test-proguard-rules.txt'
|
||||||
versionNameSuffix "-debug-" + getBranchName()
|
versionNameSuffix "-debug-" + getBranchName()
|
||||||
|
|
@ -284,7 +309,6 @@ android {
|
||||||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
||||||
|
|
||||||
dimension 'tier'
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,20 +344,17 @@ android {
|
||||||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
||||||
|
|
||||||
dimension 'tier'
|
dimension 'tier'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
disable 'ExtraTranslation'
|
|
||||||
abortOnError false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
buildToolsVersion buildToolsVersion
|
||||||
|
|
@ -341,7 +362,11 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
namespace 'fr.free.nrw.commons'
|
||||||
|
lint {
|
||||||
|
abortOnError false
|
||||||
|
disable 'MissingTranslation', 'ExtraTranslation'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTestUserName() {
|
String getTestUserName() {
|
||||||
|
|
@ -371,7 +396,3 @@ if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
# Retain declared checked exceptions for use by a Proxy instance.
|
# Retain declared checked exceptions for use by a Proxy instance.
|
||||||
-keepattributes Exceptions
|
-keepattributes Exceptions
|
||||||
|
|
||||||
|
# Note: The model package right now seems to include some other classes that
|
||||||
|
# are not used for serialization / deserialization over Gson. Hopefully
|
||||||
|
# that's not a problem since it only prevents R8 from avoiding trimming
|
||||||
|
# of few more classes.
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; }
|
||||||
|
|
||||||
# --- /Retrofit ---
|
# --- /Retrofit ---
|
||||||
|
|
||||||
# --- OkHttp + Okio ---
|
# --- OkHttp + Okio ---
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.room.Room;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import fr.free.nrw.commons.db.AppDatabase;
|
|
||||||
import fr.free.nrw.commons.review.ReviewDao;
|
|
||||||
import fr.free.nrw.commons.review.ReviewEntity;
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ReviewDaoTest {
|
|
||||||
|
|
||||||
private ReviewDao reviewDao;
|
|
||||||
private AppDatabase database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up the application database
|
|
||||||
*/
|
|
||||||
@Before
|
|
||||||
public void createDb() {
|
|
||||||
Context context = ApplicationProvider.getApplicationContext();
|
|
||||||
database = Room.inMemoryDatabaseBuilder(
|
|
||||||
context, AppDatabase.class)
|
|
||||||
.allowMainThreadQueries()
|
|
||||||
.build();
|
|
||||||
reviewDao = database.ReviewDao();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the database
|
|
||||||
*/
|
|
||||||
@After
|
|
||||||
public void closeDb() {
|
|
||||||
database.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test insertion
|
|
||||||
* Also checks isReviewedAlready():
|
|
||||||
* Case 1: When image has been reviewed/skipped by the user
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void insert() {
|
|
||||||
// Insert data
|
|
||||||
String imageId = "1234";
|
|
||||||
ReviewEntity reviewEntity = new ReviewEntity(imageId);
|
|
||||||
reviewDao.insert(reviewEntity);
|
|
||||||
|
|
||||||
// Check insertion
|
|
||||||
// Covers the case where the image exists in the database
|
|
||||||
// And isReviewedAlready() returns true
|
|
||||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
|
||||||
assertThat(isInserted, equalTo(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test review status of the image
|
|
||||||
* Case 2: When image has not been reviewed/skipped
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void isReviewedAlready(){
|
|
||||||
String imageId = "5856";
|
|
||||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
|
||||||
assertThat(isInserted, equalTo(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -234,7 +234,7 @@ class UploadTest {
|
||||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
||||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
||||||
|
|
||||||
onView(withId(R.id.btn_add_description))
|
onView(withId(R.id.btn_add))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
onView(withId(R.id.rv_descriptions)).perform(
|
onView(withId(R.id.rv_descriptions)).perform(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?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" />
|
||||||
|
|
@ -14,6 +13,8 @@
|
||||||
<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="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
@ -47,9 +48,18 @@
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
android:name=".nearby.WikidataFeedback"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:theme="@style/EditActivityTheme"
|
||||||
android:name=".description.DescriptionEditActivity"
|
android:name=".description.DescriptionEditActivity"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".edit.EditActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||||
android:process=":acra"
|
android:process=":acra"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
|
|
||||||
64
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
64
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
|
||||||
|
class BaseMarker {
|
||||||
|
private var _position: LatLng = LatLng(0.0, 0.0, 0f)
|
||||||
|
private var _title: String = ""
|
||||||
|
private var _place: Place = Place()
|
||||||
|
private var _icon: Bitmap? = null
|
||||||
|
|
||||||
|
var position: LatLng
|
||||||
|
get() = _position
|
||||||
|
set(value) {
|
||||||
|
_position = value
|
||||||
|
}
|
||||||
|
var title: String
|
||||||
|
get() = _title
|
||||||
|
set(value) {
|
||||||
|
_title = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var place: Place
|
||||||
|
get() = _place
|
||||||
|
set(value) {
|
||||||
|
_place = value
|
||||||
|
}
|
||||||
|
var icon: Bitmap?
|
||||||
|
get() = _icon
|
||||||
|
set(value) {
|
||||||
|
_icon = value
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromResource(context: Context, drawableResId: Int) {
|
||||||
|
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
||||||
|
icon = if (drawable is BitmapDrawable) {
|
||||||
|
(drawable as BitmapDrawable).bitmap
|
||||||
|
} else {
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth,
|
||||||
|
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readDouble(),
|
||||||
|
parcel.readDouble(),
|
||||||
|
parcel.readDouble()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeDouble(latitude)
|
||||||
|
parcel.writeDouble(longitude)
|
||||||
|
parcel.writeDouble(zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CameraPosition {
|
||||||
|
return CameraPosition(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CameraPosition?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.json.GsonMarshaller;
|
|
||||||
import org.wikipedia.json.GsonUnmarshaller;
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
public class CommonsAppAdapter extends AppAdapter {
|
|
||||||
private final int DEFAULT_THUMB_SIZE = 640;
|
|
||||||
private final String COOKIE_STORE_NAME = "cookie_store";
|
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
private final JsonKvStore preferences;
|
|
||||||
|
|
||||||
CommonsAppAdapter(@NonNull SessionManager sessionManager, @NonNull JsonKvStore preferences) {
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.preferences = preferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMediaWikiBaseUrl() {
|
|
||||||
return BuildConfig.COMMONS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getRestbaseUriFormat() {
|
|
||||||
return BuildConfig.COMMONS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
|
|
||||||
return OkHttpConnectionFactory.getClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDesiredLeadImageDp() {
|
|
||||||
return DEFAULT_THUMB_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoggedIn() {
|
|
||||||
return sessionManager.isUserLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUserName() {
|
|
||||||
return sessionManager.getUserName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPassword() {
|
|
||||||
return sessionManager.getPassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateAccount(@NonNull LoginResult result) {
|
|
||||||
sessionManager.updateAccount(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferenceCookieManager getCookies() {
|
|
||||||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
|
|
||||||
preferences.getString(COOKIE_STORE_NAME, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
|
|
||||||
preferences.putString(COOKIE_STORE_NAME, GsonMarshaller.marshal(cookies));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean logErrorsInsteadOfCrashing() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,22 +9,22 @@ import static org.acra.ReportField.STACK_TRACE;
|
||||||
import static org.acra.ReportField.USER_COMMENT;
|
import static org.acra.ReportField.USER_COMMENT;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.multidex.BuildConfig;
|
|
||||||
import androidx.multidex.MultiDexApplication;
|
import androidx.multidex.MultiDexApplication;
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||||
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
||||||
import com.mapbox.mapboxsdk.Mapbox;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import com.mapbox.mapboxsdk.WellKnownTileServer;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||||
|
|
@ -36,12 +36,14 @@ import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
|
||||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||||
import fr.free.nrw.commons.logging.LogUtils;
|
import fr.free.nrw.commons.logging.LogUtils;
|
||||||
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
|
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.upload.FileUtils;
|
import fr.free.nrw.commons.upload.FileUtils;
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.internal.functions.Functions;
|
import io.reactivex.internal.functions.Functions;
|
||||||
|
|
@ -59,8 +61,6 @@ import org.acra.annotation.AcraCore;
|
||||||
import org.acra.annotation.AcraDialog;
|
import org.acra.annotation.AcraDialog;
|
||||||
import org.acra.annotation.AcraMailSender;
|
import org.acra.annotation.AcraMailSender;
|
||||||
import org.acra.data.StringFormat;
|
import org.acra.data.StringFormat;
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.language.AppLanguageLookUpTable;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@AcraCore(
|
@AcraCore(
|
||||||
|
|
@ -85,6 +85,9 @@ import timber.log.Timber;
|
||||||
|
|
||||||
public class CommonsApplication extends MultiDexApplication {
|
public class CommonsApplication extends MultiDexApplication {
|
||||||
|
|
||||||
|
public static final String loginMessageIntentKey = "loginMessage";
|
||||||
|
public static final String loginUsernameIntentKey = "loginUsername";
|
||||||
|
|
||||||
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
|
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
|
||||||
@Inject
|
@Inject
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
|
|
@ -95,6 +98,9 @@ public class CommonsApplication extends MultiDexApplication {
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore defaultPrefs;
|
JsonKvStore defaultPrefs;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CommonsCookieJar cookieJar;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
|
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
|
||||||
|
|
||||||
|
|
@ -141,6 +147,11 @@ public class CommonsApplication extends MultiDexApplication {
|
||||||
*/
|
*/
|
||||||
public static Map<String, Boolean> pauseUploads = new HashMap<>();
|
public static Map<String, Boolean> pauseUploads = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory list of uploads that have been cancelled by the user
|
||||||
|
*/
|
||||||
|
public static HashSet<String> cancelledUploads = new HashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to declare and initialize various components and dependencies
|
* Used to declare and initialize various components and dependencies
|
||||||
*/
|
*/
|
||||||
|
|
@ -150,15 +161,12 @@ public class CommonsApplication extends MultiDexApplication {
|
||||||
|
|
||||||
INSTANCE = this;
|
INSTANCE = this;
|
||||||
ACRA.init(this);
|
ACRA.init(this);
|
||||||
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token), WellKnownTileServer.Mapbox);
|
|
||||||
|
|
||||||
ApplicationlessInjection
|
ApplicationlessInjection
|
||||||
.getInstance(this)
|
.getInstance(this)
|
||||||
.getCommonsApplicationComponent()
|
.getCommonsApplicationComponent()
|
||||||
.inject(this);
|
.inject(this);
|
||||||
|
|
||||||
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
|
|
||||||
|
|
||||||
initTimber();
|
initTimber();
|
||||||
|
|
||||||
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
||||||
|
|
@ -286,6 +294,7 @@ public class CommonsApplication extends MultiDexApplication {
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionManager.logout()
|
sessionManager.logout()
|
||||||
|
.andThen(Completable.fromAction(() -> cookieJar.clear()))
|
||||||
.andThen(Completable.fromAction(() -> {
|
.andThen(Completable.fromAction(() -> {
|
||||||
Timber.d("All accounts have been removed");
|
Timber.d("All accounts have been removed");
|
||||||
clearImageCache();
|
clearImageCache();
|
||||||
|
|
@ -337,4 +346,96 @@ public class CommonsApplication extends MultiDexApplication {
|
||||||
|
|
||||||
void onLogoutComplete();
|
void onLogoutComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
|
||||||
|
* with relevant intent parameters. It does not perform the actual logout operation.
|
||||||
|
*/
|
||||||
|
public static class BaseLogoutListener implements CommonsApplication.LogoutListener {
|
||||||
|
|
||||||
|
Context ctx;
|
||||||
|
String loginMessage, userName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for BaseLogoutListener.
|
||||||
|
*
|
||||||
|
* @param ctx Application context
|
||||||
|
*/
|
||||||
|
public BaseLogoutListener(final Context ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for BaseLogoutListener
|
||||||
|
*
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
* @param loginMessage Message to be displayed on the login page
|
||||||
|
* @param loginUsername Username to be pre-filled on the login page
|
||||||
|
*/
|
||||||
|
public BaseLogoutListener(final Context ctx, final String loginMessage,
|
||||||
|
final String loginUsername) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.loginMessage = loginMessage;
|
||||||
|
this.userName = loginUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutComplete() {
|
||||||
|
Timber.d("Logout complete callback received.");
|
||||||
|
final Intent loginIntent = new Intent(ctx, LoginActivity.class);
|
||||||
|
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
if (loginMessage != null) {
|
||||||
|
loginIntent.putExtra(loginMessageIntentKey, loginMessage);
|
||||||
|
}
|
||||||
|
if (userName != null) {
|
||||||
|
loginIntent.putExtra(loginUsernameIntentKey, userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.startActivity(loginIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
|
||||||
|
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
|
||||||
|
*/
|
||||||
|
public static class ActivityLogoutListener extends BaseLogoutListener {
|
||||||
|
|
||||||
|
Activity activity;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for ActivityLogoutListener.
|
||||||
|
*
|
||||||
|
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
*/
|
||||||
|
public ActivityLogoutListener(final Activity activity, final Context ctx) {
|
||||||
|
super(ctx);
|
||||||
|
this.activity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
|
||||||
|
*
|
||||||
|
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
* @param loginMessage Message to be displayed on the login page after logout.
|
||||||
|
* @param loginUsername Username to be pre-filled on the login page after logout.
|
||||||
|
*/
|
||||||
|
public ActivityLogoutListener(final Activity activity, final Context ctx,
|
||||||
|
final String loginMessage, final String loginUsername) {
|
||||||
|
super(activity, loginMessage, loginUsername);
|
||||||
|
this.activity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutComplete() {
|
||||||
|
super.onLogoutComplete();
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ package fr.free.nrw.commons.LocationPicker;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
import fr.free.nrw.commons.CameraPosition;
|
||||||
|
import fr.free.nrw.commons.Media;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for starting the activity
|
* Helper class for starting the activity
|
||||||
|
|
@ -52,6 +53,17 @@ public final class LocationPicker {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets and puts media in intent
|
||||||
|
* @param media Media
|
||||||
|
* @return LocationPicker.IntentBuilder
|
||||||
|
*/
|
||||||
|
public LocationPicker.IntentBuilder media(
|
||||||
|
final Media media) {
|
||||||
|
intent.putExtra(LocationPickerConstants.MEDIA, media);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets and sets the activity
|
* Gets and sets the activity
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
package fr.free.nrw.commons.LocationPicker;
|
||||||
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
|
|
||||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
|
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
|
||||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
|
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
|
||||||
|
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
||||||
|
|
||||||
|
import android.Manifest.permission;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.BitmapFactory;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.Location;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.location.LocationManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.animation.OvershootInterpolator;
|
import android.view.animation.OvershootInterpolator;
|
||||||
|
|
@ -28,52 +30,56 @@ import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
import androidx.appcompat.widget.AppCompatTextView;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.core.content.ContextCompat;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.mapbox.geojson.Point;
|
import fr.free.nrw.commons.CameraPosition;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition.Builder;
|
import fr.free.nrw.commons.Media;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
|
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng;
|
|
||||||
import com.mapbox.mapboxsdk.location.LocationComponent;
|
|
||||||
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
|
|
||||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback;
|
|
||||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineResult;
|
|
||||||
import com.mapbox.mapboxsdk.location.modes.CameraMode;
|
|
||||||
import com.mapbox.mapboxsdk.location.modes.RenderMode;
|
|
||||||
import com.mapbox.mapboxsdk.location.permissions.PermissionsManager;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapView;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener;
|
|
||||||
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
import com.mapbox.mapboxsdk.maps.UiSettings;
|
|
||||||
import com.mapbox.mapboxsdk.style.layers.Layer;
|
|
||||||
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
|
|
||||||
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
|
|
||||||
import fr.free.nrw.commons.MapStyle;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||||
|
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
|
||||||
|
import fr.free.nrw.commons.filepicker.Constants;
|
||||||
|
import fr.free.nrw.commons.kvstore.BasicKvStore;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
|
||||||
|
import org.osmdroid.util.GeoPoint;
|
||||||
|
import org.osmdroid.util.constants.GeoConstants;
|
||||||
|
import org.osmdroid.views.CustomZoomButtonsController;
|
||||||
|
import org.osmdroid.views.overlay.Marker;
|
||||||
|
import org.osmdroid.views.overlay.Overlay;
|
||||||
|
import org.osmdroid.views.overlay.ScaleDiskOverlay;
|
||||||
|
import org.osmdroid.views.overlay.TilesOverlay;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helps to pick location and return the result with an intent
|
* Helps to pick location and return the result with an intent
|
||||||
*/
|
*/
|
||||||
public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback,
|
public class LocationPickerActivity extends BaseActivity implements
|
||||||
OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> {
|
LocationPermissionCallback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DROPPED_MARKER_LAYER_ID : id for layer
|
* coordinateEditHelper: helps to edit coordinates
|
||||||
*/
|
*/
|
||||||
private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID";
|
@Inject
|
||||||
|
CoordinateEditHelper coordinateEditHelper;
|
||||||
|
/**
|
||||||
|
* media : Media object
|
||||||
|
*/
|
||||||
|
private Media media;
|
||||||
/**
|
/**
|
||||||
* cameraPosition : position of picker
|
* cameraPosition : position of picker
|
||||||
*/
|
*/
|
||||||
|
|
@ -83,13 +89,9 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
*/
|
*/
|
||||||
private ImageView markerImage;
|
private ImageView markerImage;
|
||||||
/**
|
/**
|
||||||
* mapboxMap : map
|
* mapView : OSM Map
|
||||||
*/
|
*/
|
||||||
private MapboxMap mapboxMap;
|
private org.osmdroid.views.MapView mapView;
|
||||||
/**
|
|
||||||
* mapView : view of the map
|
|
||||||
*/
|
|
||||||
private MapView mapView;
|
|
||||||
/**
|
/**
|
||||||
* tvAttribution : credit
|
* tvAttribution : credit
|
||||||
*/
|
*/
|
||||||
|
|
@ -98,14 +100,14 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
* activity : activity key
|
* activity : activity key
|
||||||
*/
|
*/
|
||||||
private String activity;
|
private String activity;
|
||||||
/**
|
|
||||||
* location : location
|
|
||||||
*/
|
|
||||||
private Location location;
|
|
||||||
/**
|
/**
|
||||||
* modifyLocationButton : button for start editing location
|
* modifyLocationButton : button for start editing location
|
||||||
*/
|
*/
|
||||||
Button modifyLocationButton;
|
Button modifyLocationButton;
|
||||||
|
/**
|
||||||
|
* removeLocationButton : button to remove location metadata
|
||||||
|
*/
|
||||||
|
Button removeLocationButton;
|
||||||
/**
|
/**
|
||||||
* showInMapButton : button for showing in map
|
* showInMapButton : button for showing in map
|
||||||
*/
|
*/
|
||||||
|
|
@ -118,10 +120,6 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
* fabCenterOnLocation: button for center on location;
|
* fabCenterOnLocation: button for center on location;
|
||||||
*/
|
*/
|
||||||
FloatingActionButton fabCenterOnLocation;
|
FloatingActionButton fabCenterOnLocation;
|
||||||
/**
|
|
||||||
* droppedMarkerLayer : Layer for static screen
|
|
||||||
*/
|
|
||||||
private Layer droppedMarkerLayer;
|
|
||||||
/**
|
/**
|
||||||
* shadow : imageview of shadow
|
* shadow : imageview of shadow
|
||||||
*/
|
*/
|
||||||
|
|
@ -141,19 +139,38 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
public
|
public
|
||||||
JsonKvStore applicationKvStore;
|
JsonKvStore applicationKvStore;
|
||||||
|
BasicKvStore store;
|
||||||
/**
|
/**
|
||||||
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
|
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
SystemThemeUtils systemThemeUtils;
|
SystemThemeUtils systemThemeUtils;
|
||||||
private boolean isDarkTheme;
|
private boolean isDarkTheme;
|
||||||
|
private boolean moveToCurrentLocation;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LocationServiceManager locationManager;
|
||||||
|
LocationPermissionsHelper locationPermissionsHelper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
private static final String CAMERA_POS = "cameraPosition";
|
||||||
|
private static final String ACTIVITY = "activity";
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
||||||
|
moveToCurrentLocation = false;
|
||||||
|
store = new BasicKvStore(this, "LocationPermissions");
|
||||||
|
|
||||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
|
@ -166,12 +183,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
cameraPosition = getIntent()
|
cameraPosition = getIntent()
|
||||||
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
||||||
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
|
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
|
||||||
|
media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA);
|
||||||
|
}else{
|
||||||
|
cameraPosition = savedInstanceState.getParcelable(CAMERA_POS);
|
||||||
|
activity = savedInstanceState.getString(ACTIVITY);
|
||||||
|
media = savedInstanceState.getParcelable("sMedia");
|
||||||
}
|
}
|
||||||
|
|
||||||
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
|
|
||||||
.get(LocationPickerViewModel.class);
|
|
||||||
viewModel.getResult().observe(this, this);
|
|
||||||
|
|
||||||
bindViews();
|
bindViews();
|
||||||
addBackButtonListener();
|
addBackButtonListener();
|
||||||
addPlaceSelectedButton();
|
addPlaceSelectedButton();
|
||||||
|
|
@ -179,18 +196,57 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
getToolbarUI();
|
getToolbarUI();
|
||||||
addCenterOnGPSButton();
|
addCenterOnGPSButton();
|
||||||
|
|
||||||
|
org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(),
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()));
|
||||||
|
|
||||||
|
mapView.setTileSource(TileSourceFactory.WIKIMEDIA);
|
||||||
|
mapView.setTilesScaledToDpi(true);
|
||||||
|
mapView.setMultiTouchControls(true);
|
||||||
|
|
||||||
|
org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put(
|
||||||
|
"Referer", "http://maps.wikimedia.org/"
|
||||||
|
);
|
||||||
|
mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
|
||||||
|
mapView.getController().setZoom(ZOOM_LEVEL);
|
||||||
|
mapView.setOnTouchListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||||
|
if (markerImage.getTranslationY() == 0) {
|
||||||
|
markerImage.animate().translationY(-75)
|
||||||
|
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||||
|
}
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
markerImage.animate().translationY(0)
|
||||||
|
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
if ("UploadActivity".equals(activity)) {
|
if ("UploadActivity".equals(activity)) {
|
||||||
placeSelectedButton.setVisibility(View.GONE);
|
placeSelectedButton.setVisibility(View.GONE);
|
||||||
modifyLocationButton.setVisibility(View.VISIBLE);
|
modifyLocationButton.setVisibility(View.VISIBLE);
|
||||||
|
removeLocationButton.setVisibility(View.VISIBLE);
|
||||||
showInMapButton.setVisibility(View.VISIBLE);
|
showInMapButton.setVisibility(View.VISIBLE);
|
||||||
largeToolbarText.setText(getResources().getString(R.string.image_location));
|
largeToolbarText.setText(getResources().getString(R.string.image_location));
|
||||||
smallToolbarText.setText(getResources().
|
smallToolbarText.setText(getResources().
|
||||||
getString(R.string.check_whether_location_is_correct));
|
getString(R.string.check_whether_location_is_correct));
|
||||||
fabCenterOnLocation.setVisibility(View.GONE);
|
fabCenterOnLocation.setVisibility(View.GONE);
|
||||||
|
markerImage.setVisibility(View.GONE);
|
||||||
|
shadow.setVisibility(View.GONE);
|
||||||
|
assert cameraPosition != null;
|
||||||
|
showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(),
|
||||||
|
cameraPosition.getLongitude()));
|
||||||
}
|
}
|
||||||
|
setupMapView();
|
||||||
|
|
||||||
mapView.onCreate(savedInstanceState);
|
if("UploadActivity".equals(activity)){
|
||||||
mapView.getMapAsync(this);
|
if(mapView != null && mapView.getController() != null && cameraPosition != null){
|
||||||
|
GeoPoint cameraGeoPoint = new GeoPoint(cameraPosition.getLatitude(),
|
||||||
|
cameraPosition.getLongitude());
|
||||||
|
|
||||||
|
mapView.getController().setCenter(cameraGeoPoint);
|
||||||
|
mapView.getController().animateTo(cameraGeoPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -201,12 +257,26 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
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
|
|
||||||
* @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();
|
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,131 +318,68 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -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() + "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (media == null) {
|
||||||
final Intent returningIntent = new Intent();
|
final Intent returningIntent = new Intent();
|
||||||
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
||||||
mapboxMap.getCameraPosition());
|
new CameraPosition(mapView.getMapCenter().getLatitude(),
|
||||||
|
mapView.getMapCenter().getLongitude(), 14.0));
|
||||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
||||||
|
} else {
|
||||||
|
updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()),
|
||||||
|
String.valueOf(mapView.getMapCenter().getLongitude()),
|
||||||
|
String.valueOf(0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetched coordinates are replaced with existing coordinates by a POST API call.
|
||||||
|
* @param Latitude to be added
|
||||||
|
* @param Longitude to be added
|
||||||
|
* @param Accuracy to be added
|
||||||
|
*/
|
||||||
|
public void updateCoordinates(final String Latitude, final String Longitude,
|
||||||
|
final String Accuracy) {
|
||||||
|
if (media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
compositeDisposable.add(
|
||||||
|
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
|
||||||
|
Latitude, Longitude, Accuracy)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(s -> {
|
||||||
|
Timber.d("Coordinates are added.");
|
||||||
|
}));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) {
|
||||||
|
final String username = sessionManager.getUserName();
|
||||||
|
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||||
|
this,
|
||||||
|
getString(R.string.invalid_login_message),
|
||||||
|
username
|
||||||
|
);
|
||||||
|
|
||||||
|
CommonsApplication.getInstance().clearApplicationData(
|
||||||
|
this, logoutListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Center the camera on the last saved location
|
* Center the camera on the last saved location
|
||||||
*/
|
*/
|
||||||
private void addCenterOnGPSButton(){
|
private void addCenterOnGPSButton() {
|
||||||
fabCenterOnLocation = findViewById(R.id.center_on_gps);
|
fabCenterOnLocation = findViewById(R.id.center_on_gps);
|
||||||
fabCenterOnLocation.setOnClickListener(view -> getCenter());
|
fabCenterOnLocation.setOnClickListener(view -> {
|
||||||
|
moveToCurrentLocation = true;
|
||||||
|
requestLocationPermissions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animate map to move to desired Latitude and Longitude
|
* Adds selected location marker on the map
|
||||||
*/
|
*/
|
||||||
void getCenter() {
|
private void showSelectedLocationMarker(GeoPoint point) {
|
||||||
mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(),location.getLongitude()),15.0));
|
Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker);
|
||||||
|
Marker marker = new Marker(mapView);
|
||||||
|
marker.setPosition(point);
|
||||||
|
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
|
||||||
|
marker.setIcon(icon);
|
||||||
|
marker.setInfoWindow(null);
|
||||||
|
mapView.getOverlays().add(marker);
|
||||||
|
mapView.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes selected location marker from the map
|
||||||
|
*/
|
||||||
|
private void removeSelectedLocationMarker() {
|
||||||
|
List<Overlay> overlays = mapView.getOverlays();
|
||||||
|
for (int i = 0; i < overlays.size(); i++) {
|
||||||
|
if (overlays.get(i) instanceof Marker) {
|
||||||
|
Marker item = (Marker) overlays.get(i);
|
||||||
|
if (cameraPosition.getLatitude() == item.getPosition().getLatitude()
|
||||||
|
&& cameraPosition.getLongitude() == item.getPosition().getLongitude()) {
|
||||||
|
mapView.getOverlays().remove(i);
|
||||||
|
mapView.invalidate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Center the map at user's current location
|
||||||
|
*/
|
||||||
|
private void requestLocationPermissions() {
|
||||||
|
locationPermissionsHelper = new LocationPermissionsHelper(
|
||||||
|
this, locationManager, this);
|
||||||
|
locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title,
|
||||||
|
R.string.upload_map_location_access);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
public void onRequestPermissionsResult(final int requestCode,
|
||||||
super.onStart();
|
@NonNull final String[] permissions,
|
||||||
mapView.onStart();
|
@NonNull final int[] grantResults) {
|
||||||
|
if (requestCode == Constants.RequestCodes.LOCATION
|
||||||
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
onLocationPermissionGranted();
|
||||||
|
} else {
|
||||||
|
onLocationPermissionDenied(getString(R.string.upload_map_location_access));
|
||||||
|
}
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -478,26 +532,102 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
public void onLocationPermissionDenied(String toastMessage) {
|
||||||
super.onStop();
|
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||||
mapView.onStop();
|
permission.ACCESS_FINE_LOCATION)) {
|
||||||
|
if (!locationPermissionsHelper.checkLocationPermission(this)) {
|
||||||
|
if (store.getBoolean("isPermissionDenied", false)) {
|
||||||
|
// means user has denied location permission twice or checked the "Don't show again"
|
||||||
|
locationPermissionsHelper.showAppSettingsDialog(this,
|
||||||
|
R.string.upload_map_location_access);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
store.putBoolean("isPermissionDenied", true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(final @NotNull Bundle outState) {
|
public void onLocationPermissionGranted() {
|
||||||
|
if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) {
|
||||||
|
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||||
|
locationManager.requestLocationUpdatesFromProvider(
|
||||||
|
LocationManager.NETWORK_PROVIDER);
|
||||||
|
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||||
|
getLocation();
|
||||||
|
} else {
|
||||||
|
getLocation();
|
||||||
|
locationPermissionsHelper.showLocationOffDialog(this,
|
||||||
|
R.string.ask_to_turn_location_on_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets new location if locations services are on, else gets last location
|
||||||
|
*/
|
||||||
|
private void getLocation() {
|
||||||
|
fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
|
||||||
|
if (currLocation != null) {
|
||||||
|
GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(),
|
||||||
|
currLocation.getLongitude());
|
||||||
|
addLocationMarker(currLocationGeopoint);
|
||||||
|
mapView.getController().setCenter(currLocationGeopoint);
|
||||||
|
mapView.getController().animateTo(currLocationGeopoint);
|
||||||
|
markerImage.setTranslationY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLocationMarker(GeoPoint geoPoint) {
|
||||||
|
if (moveToCurrentLocation) {
|
||||||
|
mapView.getOverlays().clear();
|
||||||
|
}
|
||||||
|
ScaleDiskOverlay diskOverlay =
|
||||||
|
new ScaleDiskOverlay(this,
|
||||||
|
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
|
||||||
|
Paint circlePaint = new Paint();
|
||||||
|
circlePaint.setColor(Color.rgb(128, 128, 128));
|
||||||
|
circlePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
circlePaint.setStrokeWidth(2f);
|
||||||
|
diskOverlay.setCirclePaint2(circlePaint);
|
||||||
|
Paint diskPaint = new Paint();
|
||||||
|
diskPaint.setColor(Color.argb(40, 128, 128, 128));
|
||||||
|
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
diskOverlay.setCirclePaint1(diskPaint);
|
||||||
|
diskOverlay.setDisplaySizeMin(900);
|
||||||
|
diskOverlay.setDisplaySizeMax(1700);
|
||||||
|
mapView.getOverlays().add(diskOverlay);
|
||||||
|
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
|
||||||
|
mapView);
|
||||||
|
startMarker.setPosition(geoPoint);
|
||||||
|
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
|
||||||
|
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
|
||||||
|
startMarker.setIcon(
|
||||||
|
ContextCompat.getDrawable(this, R.drawable.current_location_marker));
|
||||||
|
startMarker.setTitle("Your Location");
|
||||||
|
startMarker.setTextLabelFontSize(24);
|
||||||
|
mapView.getOverlays().add(startMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the state of the activity
|
||||||
|
* @param outState Bundle
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
mapView.onSaveInstanceState(outState);
|
if(cameraPosition!=null){
|
||||||
|
outState.putParcelable(CAMERA_POS, cameraPosition);
|
||||||
|
}
|
||||||
|
if(activity!=null){
|
||||||
|
outState.putString(ACTIVITY, activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if(media!=null){
|
||||||
protected void onDestroy() {
|
outState.putParcelable("sMedia", media);
|
||||||
super.onDestroy();
|
|
||||||
mapView.onDestroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLowMemory() {
|
|
||||||
super.onLowMemory();
|
|
||||||
mapView.onLowMemory();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
package fr.free.nrw.commons.LocationPicker;
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants need for location picking
|
* Constants need for location picking
|
||||||
*/
|
*/
|
||||||
|
|
@ -13,6 +11,9 @@ public final class LocationPickerConstants {
|
||||||
public static final String MAP_CAMERA_POSITION
|
public static final String MAP_CAMERA_POSITION
|
||||||
= "location.picker.cameraPosition";
|
= "location.picker.cameraPosition";
|
||||||
|
|
||||||
|
public static final String MEDIA
|
||||||
|
= "location.picker.media";
|
||||||
|
|
||||||
|
|
||||||
private LocationPickerConstants() {
|
private LocationPickerConstants() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import android.app.Application;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
import fr.free.nrw.commons.CameraPosition;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public abstract class MapController {
|
||||||
public class NearbyPlacesInfo {
|
public class NearbyPlacesInfo {
|
||||||
public List<Place> placeList; // List of nearby places
|
public List<Place> placeList; // List of nearby places
|
||||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||||
public LatLng curLatLng; // Current location when this places are populated
|
public LatLng currentLatLng; // Current location when this places are populated
|
||||||
public LatLng searchLatLng; // Search location for finding this places
|
public LatLng searchLatLng; // Search location for finding this places
|
||||||
public List<Media> mediaList; // Search location for finding this places
|
public List<Media> mediaList; // Search location for finding this places
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ public abstract class MapController {
|
||||||
public class ExplorePlacesInfo {
|
public class ExplorePlacesInfo {
|
||||||
public List<Place> explorePlaceList; // List of nearby places
|
public List<Place> explorePlaceList; // List of nearby places
|
||||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||||
public LatLng curLatLng; // Current location when this places are populated
|
public LatLng currentLatLng; // Current location when this places are populated
|
||||||
public LatLng searchLatLng; // Search location for finding this places
|
public LatLng searchLatLng; // Search location for finding this places
|
||||||
public List<Media> mediaList; // Search location for finding this places
|
public List<Media> mediaList; // Search location for finding this places
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants for various map styles
|
|
||||||
*/
|
|
||||||
public final class MapStyle {
|
|
||||||
public static final String DARK = Style.getPredefinedStyle("Dark");
|
|
||||||
public static final String OUTDOORS = Style.getPredefinedStyle("Outdoors");
|
|
||||||
public static final String STREETS = Style.getPredefinedStyle("Streets");
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,8 @@ package fr.free.nrw.commons
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryPage
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||||
import org.wikipedia.page.PageTitle
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien
|
||||||
return Single.ambArray(
|
return Single.ambArray(
|
||||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||||
.onErrorResumeNext { Single.never() },
|
.onErrorResumeNext { Single.never() },
|
||||||
mediaClient.getMedia(media.filename)
|
mediaClient.getMediaSuppressingErrors(media.filename)
|
||||||
.onErrorResumeNext { Single.never() }
|
.onErrorResumeNext { Single.never() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -15,28 +15,26 @@ import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
import okhttp3.logging.HttpLoggingInterceptor;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
||||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
|
||||||
import org.wikipedia.dataclient.okhttp.HttpStatusException;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public final class OkHttpConnectionFactory {
|
public final class OkHttpConnectionFactory {
|
||||||
private static final String CACHE_DIR_NAME = "okhttp-cache";
|
private static final String CACHE_DIR_NAME = "okhttp-cache";
|
||||||
private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
|
private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
|
||||||
@NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(),
|
|
||||||
CACHE_DIR_NAME), NET_CACHE_SIZE);
|
|
||||||
|
|
||||||
@NonNull
|
public static OkHttpClient CLIENT;
|
||||||
private static final OkHttpClient CLIENT = createClient();
|
|
||||||
|
|
||||||
@NonNull public static OkHttpClient getClient() {
|
@NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) {
|
||||||
|
if (CLIENT == null) {
|
||||||
|
CLIENT = createClient(cookieJar);
|
||||||
|
}
|
||||||
return CLIENT;
|
return CLIENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static OkHttpClient createClient() {
|
private static OkHttpClient createClient(final CommonsCookieJar cookieJar) {
|
||||||
return new OkHttpClient.Builder()
|
return new OkHttpClient.Builder()
|
||||||
.cookieJar(SharedPreferenceCookieManager.getInstance())
|
.cookieJar(cookieJar)
|
||||||
.cache(NET_CACHE)
|
.cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null)
|
||||||
.connectTimeout(120, TimeUnit.SECONDS)
|
.connectTimeout(120, TimeUnit.SECONDS)
|
||||||
.writeTimeout(120, TimeUnit.SECONDS)
|
.writeTimeout(120, TimeUnit.SECONDS)
|
||||||
.readTimeout(120, TimeUnit.SECONDS)
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
|
@ -69,6 +67,8 @@ public final class OkHttpConnectionFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
||||||
|
private static final String SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log";
|
||||||
|
public static final String SUPPRESS_ERROR_LOG_HEADER = SUPPRESS_ERROR_LOG+": true";
|
||||||
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
|
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
|
||||||
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
|
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
|
||||||
|
|
||||||
|
|
@ -77,7 +77,16 @@ public final class OkHttpConnectionFactory {
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Response intercept(@NonNull final Chain chain) throws IOException {
|
public Response intercept(@NonNull final Chain chain) throws IOException {
|
||||||
final Response rsp = chain.proceed(chain.request());
|
final Request rq = chain.request();
|
||||||
|
|
||||||
|
// If the request contains our special "suppress errors" header, make note of it
|
||||||
|
// but don't pass that on to the server.
|
||||||
|
final boolean suppressErrors = rq.headers().names().contains(SUPPRESS_ERROR_LOG);
|
||||||
|
final Request request = rq.newBuilder()
|
||||||
|
.removeHeader(SUPPRESS_ERROR_LOG)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Response rsp = chain.proceed(request);
|
||||||
|
|
||||||
// Do not intercept certain requests and let the caller handle the errors
|
// Do not intercept certain requests and let the caller handle the errors
|
||||||
if(isExcludedUrl(chain.request())) {
|
if(isExcludedUrl(chain.request())) {
|
||||||
|
|
@ -91,8 +100,13 @@ public final class OkHttpConnectionFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
|
// Log the error as debug (and therefore, "expected") or at error level
|
||||||
|
if (suppressErrors) {
|
||||||
|
Timber.d(e, "Suppressed (known / expected) error");
|
||||||
|
} else {
|
||||||
Timber.e(e);
|
Timber.e(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rsp;
|
return rsp;
|
||||||
}
|
}
|
||||||
throw new HttpStatusException(rsp);
|
throw new HttpStatusException(rsp);
|
||||||
|
|
@ -111,4 +125,30 @@ public final class OkHttpConnectionFactory {
|
||||||
|
|
||||||
private OkHttpConnectionFactory() {
|
private OkHttpConnectionFactory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HttpStatusException extends IOException {
|
||||||
|
private final int code;
|
||||||
|
private final String url;
|
||||||
|
public HttpStatusException(@NonNull Response rsp) {
|
||||||
|
this.code = rsp.code();
|
||||||
|
this.url = rsp.request().url().uri().toString();
|
||||||
|
try {
|
||||||
|
if (rsp.body() != null && rsp.body().contentType() != null
|
||||||
|
&& rsp.body().contentType().toString().contains("json")) {
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int code() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
String str = "Code: " + code + ", URL: " + url;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,16 @@ import android.text.SpannableString;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||||
import org.wikipedia.page.PageTitle;
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -31,9 +29,6 @@ import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
|
||||||
import static fr.free.nrw.commons.campaigns.CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE;
|
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
public static PageTitle getPageTitle(@NonNull String title) {
|
public static PageTitle getPageTitle(@NonNull String title) {
|
||||||
|
|
@ -137,12 +132,6 @@ public class Utils {
|
||||||
*/
|
*/
|
||||||
public static void handleWebUrl(Context context, Uri url) {
|
public static void handleWebUrl(Context context, Uri url) {
|
||||||
Timber.d("Launching web url %s", url.toString());
|
Timber.d("Launching web url %s", url.toString());
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
|
|
||||||
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
|
|
||||||
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder()
|
final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder()
|
||||||
.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor))
|
.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.Html;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response of the Thanks API.
|
||||||
|
* Context:
|
||||||
|
* The Commons Android app lets you thank other contributors who have uploaded a great picture.
|
||||||
|
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||||
|
*/
|
||||||
|
class MwThankPostResponse : MwResponse() {
|
||||||
|
var result: Result? = null
|
||||||
|
|
||||||
|
inner class Result {
|
||||||
|
var success: Int? = null
|
||||||
|
var recipient: String? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import org.wikipedia.csrf.CsrfTokenClient
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class acts as a Client to facilitate wiki page editing
|
* This class acts as a Client to facilitate wiki page editing
|
||||||
|
|
@ -25,12 +27,50 @@ 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) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
Observable.just(false)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append text to the end of a wiki page
|
* Append text to the end of a wiki page
|
||||||
|
|
@ -41,12 +81,16 @@ 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) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend text to the beginning of a wiki page
|
* Prepend text to the beginning of a wiki page
|
||||||
|
|
@ -57,12 +101,39 @@ 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) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
Observable.just(false)
|
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
|
||||||
|
|
@ -76,11 +147,16 @@ 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) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
Observable.just(0)
|
Observable.just(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whole WikiText of required file
|
* Get whole WikiText of required 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§ion=new")
|
||||||
|
fun postNewSection(
|
||||||
|
@Field("title") title: String,
|
||||||
|
@Field("summary") summary: String,
|
||||||
|
@Field("sectiontitle") sectionTitle: String,
|
||||||
|
@Field("text") sectionText: String,
|
||||||
|
@Field("token") token: String
|
||||||
|
): Observable<Edit>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
@POST(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
||||||
fun postCaptions(
|
fun postCaptions(
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
|
|
@ -91,10 +128,7 @@ interface PageEditInterface {
|
||||||
* @param titles : Name of the file
|
* @param titles : Name of the file
|
||||||
* @return Single<MwQueryResult>
|
* @return Single<MwQueryResult>
|
||||||
*/
|
*/
|
||||||
@GET(
|
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||||
Service.MW_API_PREFIX +
|
|
||||||
"action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles="
|
|
||||||
)
|
|
||||||
fun getWikiText(
|
fun getWikiText(
|
||||||
@Query("titles") title: String
|
@Query("titles") title: String
|
||||||
): Single<MwQueryResponse?>
|
): Single<MwQueryResponse?>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ package fr.free.nrw.commons.actions
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import org.wikipedia.csrf.CsrfTokenClient
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
import org.wikipedia.dataclient.Service
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import org.wikipedia.dataclient.mwapi.MwPostResponse
|
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -17,7 +17,7 @@ import javax.inject.Singleton
|
||||||
@Singleton
|
@Singleton
|
||||||
class ThanksClient @Inject constructor(
|
class ThanksClient @Inject constructor(
|
||||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||||
@param:Named("commons-service") private val service: Service
|
private val service: ThanksInterface
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Thanks a user for a particular revision
|
* Thanks a user for a particular revision
|
||||||
|
|
@ -26,11 +26,23 @@ class ThanksClient @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun thank(revisionId: Long): Observable<Boolean> {
|
fun thank(revisionId: Long): Observable<Boolean> {
|
||||||
return try {
|
return try {
|
||||||
service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent)
|
service.thank(
|
||||||
.map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 }
|
revisionId.toString(), // Rev
|
||||||
} catch (throwable: Throwable) {
|
null, // Log
|
||||||
|
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)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thanks API.
|
||||||
|
* Context:
|
||||||
|
* The Commons Android app lets you thank another contributor who has uploaded a great picture.
|
||||||
|
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||||
|
*/
|
||||||
|
interface ThanksInterface {
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=thank")
|
||||||
|
fun thank(
|
||||||
|
@Field("rev") rev: String?,
|
||||||
|
@Field("log") log: String?,
|
||||||
|
@Field("token") token: String,
|
||||||
|
@Field("source") source: String?
|
||||||
|
): Observable<MwThankPostResponse?>
|
||||||
|
}
|
||||||
|
|
@ -14,10 +14,8 @@ import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
|
import android.widget.TextView;
|
||||||
import androidx.annotation.ColorRes;
|
import androidx.annotation.ColorRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -26,31 +24,20 @@ import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.core.app.NavUtils;
|
import androidx.core.app.NavUtils;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import fr.free.nrw.commons.auth.login.LoginResult;
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
|
||||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
import fr.free.nrw.commons.utils.ActivityUtils;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import org.wikipedia.AppAdapter;
|
import fr.free.nrw.commons.auth.login.LoginCallback;
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.login.LoginClient;
|
|
||||||
import org.wikipedia.login.LoginClient.LoginCallback;
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import butterknife.OnEditorAction;
|
|
||||||
import butterknife.OnFocusChange;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
|
@ -58,25 +45,19 @@ import fr.free.nrw.commons.utils.ConfigUtils;
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
|
import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey;
|
||||||
|
import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey;
|
||||||
|
|
||||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
|
||||||
WikiSite commonsWikiSite;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
JsonKvStore applicationKvStore;
|
JsonKvStore applicationKvStore;
|
||||||
|
|
@ -87,39 +68,16 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
@Inject
|
@Inject
|
||||||
SystemThemeUtils systemThemeUtils;
|
SystemThemeUtils systemThemeUtils;
|
||||||
|
|
||||||
@BindView(R.id.login_button)
|
private ActivityLoginBinding binding;
|
||||||
Button loginButton;
|
|
||||||
|
|
||||||
@BindView(R.id.login_username)
|
|
||||||
EditText usernameEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.login_password)
|
|
||||||
EditText passwordEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.login_two_factor)
|
|
||||||
EditText twoFactorEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.error_message_container)
|
|
||||||
ViewGroup errorMessageContainer;
|
|
||||||
|
|
||||||
@BindView(R.id.error_message)
|
|
||||||
TextView errorMessage;
|
|
||||||
|
|
||||||
@BindView(R.id.login_credentials)
|
|
||||||
TextView loginCredentials;
|
|
||||||
|
|
||||||
@BindView(R.id.two_factor_container)
|
|
||||||
TextInputLayout twoFactorContainer;
|
|
||||||
|
|
||||||
ProgressDialog progressDialog;
|
ProgressDialog progressDialog;
|
||||||
private AppCompatDelegate delegate;
|
private AppCompatDelegate delegate;
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
private Call<MwQueryResponse> loginToken;
|
|
||||||
final String saveProgressDailog="ProgressDailog_state";
|
final String saveProgressDailog="ProgressDailog_state";
|
||||||
final String saveErrorMessage ="errorMessage";
|
final String saveErrorMessage ="errorMessage";
|
||||||
final String saveUsername="username";
|
final String saveUsername="username";
|
||||||
final String savePassword="password";
|
final String savePassword="password";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -133,31 +91,50 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
getDelegate().installViewFactory();
|
getDelegate().installViewFactory();
|
||||||
getDelegate().onCreate(savedInstanceState);
|
getDelegate().onCreate(savedInstanceState);
|
||||||
|
|
||||||
setContentView(R.layout.activity_login);
|
binding = ActivityLoginBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
ButterKnife.bind(this);
|
String message = getIntent().getStringExtra(loginMessageIntentKey);
|
||||||
|
String username = getIntent().getStringExtra(loginUsernameIntentKey);
|
||||||
|
|
||||||
usernameEdit.addTextChangedListener(textWatcher);
|
binding.loginUsername.addTextChangedListener(textWatcher);
|
||||||
passwordEdit.addTextChangedListener(textWatcher);
|
binding.loginPassword.addTextChangedListener(textWatcher);
|
||||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
binding.loginTwoFactor.addTextChangedListener(textWatcher);
|
||||||
|
|
||||||
|
binding.skipLogin.setOnClickListener(view -> skipLogin());
|
||||||
|
binding.forgotPassword.setOnClickListener(view -> forgotPassword());
|
||||||
|
binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked());
|
||||||
|
binding.signUpButton.setOnClickListener(view -> signUp());
|
||||||
|
binding.loginButton.setOnClickListener(view -> performLogin());
|
||||||
|
|
||||||
|
binding.loginPassword.setOnEditorActionListener(this::onEditorAction);
|
||||||
|
binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged);
|
||||||
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
if (ConfigUtils.isBetaFlavour()) {
|
||||||
loginCredentials.setText(getString(R.string.login_credential));
|
binding.loginCredentials.setText(getString(R.string.login_credential));
|
||||||
} else {
|
} else {
|
||||||
loginCredentials.setVisibility(View.GONE);
|
binding.loginCredentials.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
showMessage(message, R.color.secondaryDarkColor);
|
||||||
|
}
|
||||||
|
if (username != null) {
|
||||||
|
binding.loginUsername.setText(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
@OnFocusChange(R.id.login_password)
|
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
|
||||||
|
* @param view The keyboard
|
||||||
|
* @param hasFocus Set to true if the keyboard has focus
|
||||||
|
*/
|
||||||
void onPasswordFocusChanged(View view, boolean hasFocus) {
|
void onPasswordFocusChanged(View view, boolean hasFocus) {
|
||||||
if (!hasFocus) {
|
if (!hasFocus) {
|
||||||
ViewUtil.hideKeyboard(view);
|
ViewUtil.hideKeyboard(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEditorAction(R.id.login_password)
|
boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||||
boolean onEditorAction(int actionId, KeyEvent keyEvent) {
|
if (binding.loginButton.isEnabled()) {
|
||||||
if (loginButton.isEnabled()) {
|
|
||||||
if (actionId == IME_ACTION_DONE) {
|
if (actionId == IME_ACTION_DONE) {
|
||||||
performLogin();
|
performLogin();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -170,8 +147,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OnClick(R.id.skip_login)
|
protected void skipLogin() {
|
||||||
void skipLogin() {
|
|
||||||
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
|
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
|
||||||
.setMessage(R.string.skip_login_message)
|
.setMessage(R.string.skip_login_message)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
|
|
@ -183,18 +159,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.forgot_password)
|
protected void forgotPassword() {
|
||||||
void forgotPassword() {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.about_privacy_policy)
|
protected void onPrivacyPolicyClicked() {
|
||||||
void onPrivacyPolicyClicked() {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.sign_up_button)
|
protected void signUp() {
|
||||||
void signUp() {
|
|
||||||
Intent intent = new Intent(this, SignupActivity.class);
|
Intent intent = new Intent(this, SignupActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
@ -232,76 +205,65 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
usernameEdit.removeTextChangedListener(textWatcher);
|
binding.loginUsername.removeTextChangedListener(textWatcher);
|
||||||
passwordEdit.removeTextChangedListener(textWatcher);
|
binding.loginPassword.removeTextChangedListener(textWatcher);
|
||||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
binding.loginTwoFactor.removeTextChangedListener(textWatcher);
|
||||||
delegate.onDestroy();
|
delegate.onDestroy();
|
||||||
if(null!=loginClient) {
|
if(null!=loginClient) {
|
||||||
loginClient.cancel();
|
loginClient.cancel();
|
||||||
}
|
}
|
||||||
|
binding = null;
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.login_button)
|
|
||||||
public void performLogin() {
|
public void performLogin() {
|
||||||
Timber.d("Login to start!");
|
Timber.d("Login to start!");
|
||||||
final String username = usernameEdit.getText().toString();
|
final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString();
|
||||||
final String rawUsername = usernameEdit.getText().toString().trim();
|
final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString();
|
||||||
final String password = passwordEdit.getText().toString();
|
final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString();
|
||||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
|
||||||
|
|
||||||
showLoggingProgressBar();
|
showLoggingProgressBar();
|
||||||
doLogin(username, password, twoFactorCode);
|
loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(),
|
||||||
}
|
new LoginCallback() {
|
||||||
|
|
||||||
private void doLogin(String username, String password, String twoFactorCode) {
|
|
||||||
progressDialog.show();
|
|
||||||
loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken();
|
|
||||||
loginToken.enqueue(
|
|
||||||
new Callback<MwQueryResponse>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<MwQueryResponse> call,
|
public void success(@NonNull LoginResult loginResult) {
|
||||||
Response<MwQueryResponse> response) {
|
runOnUiThread(()->{
|
||||||
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");
|
Timber.d("Login Success");
|
||||||
onLoginSuccess(result);
|
hideProgress();
|
||||||
|
onLoginSuccess(loginResult);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void twoFactorPrompt(@NonNull Throwable caught,
|
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
|
||||||
@Nullable String token) {
|
runOnUiThread(()->{
|
||||||
Timber.d("Requesting 2FA prompt");
|
Timber.d("Requesting 2FA prompt");
|
||||||
hideProgress();
|
hideProgress();
|
||||||
askUserForTwoFactorAuth();
|
askUserForTwoFactorAuth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void passwordResetPrompt(@Nullable String token) {
|
public void passwordResetPrompt(@Nullable String token) {
|
||||||
|
runOnUiThread(()->{
|
||||||
Timber.d("Showing password reset prompt");
|
Timber.d("Showing password reset prompt");
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showPasswordResetPrompt();
|
showPasswordResetPrompt();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void error(@NonNull Throwable caught) {
|
public void error(@NonNull Throwable caught) {
|
||||||
|
runOnUiThread(()->{
|
||||||
Timber.e(caught);
|
Timber.e(caught);
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showMessageAndCancelDialog(caught.getLocalizedMessage());
|
showMessageAndCancelDialog(caught.getLocalizedMessage());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call<MwQueryResponse> call, Throwable t) {
|
|
||||||
Timber.e(t);
|
|
||||||
showMessageAndCancelDialog(t.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideProgress() {
|
private void hideProgress() {
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
|
|
@ -332,13 +294,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoginSuccess(LoginResult loginResult) {
|
private void onLoginSuccess(LoginResult loginResult) {
|
||||||
if (!progressDialog.isShowing()) {
|
|
||||||
// no longer attached to activity!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
compositeDisposable.clear();
|
compositeDisposable.clear();
|
||||||
sessionManager.setUserLoggedIn(true);
|
sessionManager.setUserLoggedIn(true);
|
||||||
AppAdapter.get().updateAccount(loginResult);
|
sessionManager.updateAccount(loginResult);
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
showSuccessAndDismissDialog();
|
showSuccessAndDismissDialog();
|
||||||
startMainActivity();
|
startMainActivity();
|
||||||
|
|
@ -385,9 +343,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
public void askUserForTwoFactorAuth() {
|
||||||
progressDialog.dismiss();
|
progressDialog.dismiss();
|
||||||
twoFactorContainer.setVisibility(VISIBLE);
|
binding.twoFactorContainer.setVisibility(VISIBLE);
|
||||||
twoFactorEdit.setVisibility(VISIBLE);
|
binding.loginTwoFactor.setVisibility(VISIBLE);
|
||||||
twoFactorEdit.requestFocus();
|
binding.loginTwoFactor.requestFocus();
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||||
|
|
@ -418,15 +376,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||||
errorMessage.setText(getString(resId));
|
binding.errorMessage.setText(getString(resId));
|
||||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||||
errorMessageContainer.setVisibility(VISIBLE);
|
binding.errorMessageContainer.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showMessage(String message, @ColorRes int colorResId) {
|
private void showMessage(String message, @ColorRes int colorResId) {
|
||||||
errorMessage.setText(message);
|
binding.errorMessage.setText(message);
|
||||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||||
errorMessageContainer.setVisibility(VISIBLE);
|
binding.errorMessageContainer.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppCompatDelegate getDelegate() {
|
private AppCompatDelegate getDelegate() {
|
||||||
|
|
@ -447,9 +405,11 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable editable) {
|
public void afterTextChanged(Editable editable) {
|
||||||
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
boolean enabled = binding.loginUsername.getText().length() != 0 &&
|
||||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
binding.loginPassword.getText().length() != 0 &&
|
||||||
loginButton.setEnabled(enabled);
|
(BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 ||
|
||||||
|
binding.loginTwoFactor.getVisibility() != VISIBLE);
|
||||||
|
binding.loginButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,22 +427,22 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
||||||
} else {
|
} else {
|
||||||
outState.putBoolean(saveProgressDailog,false);
|
outState.putBoolean(saveProgressDailog,false);
|
||||||
}
|
}
|
||||||
outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage
|
outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage
|
||||||
outState.putString(saveUsername,getUsername()); // Save the username
|
outState.putString(saveUsername,getUsername()); // Save the username
|
||||||
outState.putString(savePassword,getPassword()); // Save the password
|
outState.putString(savePassword,getPassword()); // Save the password
|
||||||
}
|
}
|
||||||
private String getUsername() {
|
private String getUsername() {
|
||||||
return usernameEdit.getText().toString();
|
return binding.loginUsername.getText().toString();
|
||||||
}
|
}
|
||||||
private String getPassword(){
|
private String getPassword(){
|
||||||
return passwordEdit.getText().toString();
|
return binding.loginPassword.getText().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
usernameEdit.setText(savedInstanceState.getString(saveUsername));
|
binding.loginUsername.setText(savedInstanceState.getString(saveUsername));
|
||||||
passwordEdit.setText(savedInstanceState.getString(savePassword));
|
binding.loginPassword.setText(savedInstanceState.getString(savePassword));
|
||||||
if(savedInstanceState.getBoolean(saveProgressDailog)) {
|
if(savedInstanceState.getBoolean(saveProgressDailog)) {
|
||||||
performLogin();
|
performLogin();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
|
|
||||||
import org.wikipedia.dataclient.Service;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for logout
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class LogoutClient {
|
|
||||||
|
|
||||||
private final Service service;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public LogoutClient(@Named("commons-service") Service service) {
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the CSRF token and uses that to post the logout api call
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Observable<MwPostResponse> postLogout() {
|
|
||||||
return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout(
|
|
||||||
Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,8 +9,7 @@ import android.text.TextUtils;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.wikipedia.login.LoginResult;
|
import fr.free.nrw.commons.auth.login.LoginResult;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
@ -123,18 +122,18 @@ public class SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Clears existing accounts from account manager
|
* Returns a Completable that clears existing accounts from account manager
|
||||||
* 2. Calls MediaWikiApi's logout function to clear cookies
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public Completable logout() {
|
public Completable logout() {
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
return Completable.fromObservable(
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
Observable.empty()
|
||||||
return Completable.fromObservable(Observable.fromArray(allAccounts)
|
.doOnComplete(
|
||||||
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
|
() -> {
|
||||||
.doOnComplete(() -> {
|
removeAccount();
|
||||||
currentAccount = null;
|
currentAccount = null;
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package fr.free.nrw.commons.auth;
|
package fr.free.nrw.commons.auth;
|
||||||
|
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.webkit.WebSettings;
|
import android.webkit.WebSettings;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
|
|
@ -61,4 +63,20 @@ public class SignupActivity extends BaseActivity {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
|
||||||
|
* https://issuetracker.google.com/issues/141132133
|
||||||
|
* App tries to put light/dark theme to webview and crashes in the process
|
||||||
|
* This code tries to prevent applying the theme when sdk is between api 21 to 25
|
||||||
|
* @param overrideConfiguration
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void applyOverrideConfiguration(final Configuration overrideConfiguration) {
|
||||||
|
if (Build.VERSION.SDK_INT <= 25 &&
|
||||||
|
(getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.applyOverrideConfiguration(overrideConfiguration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.concurrent.Executors.newSingleThreadExecutor
|
||||||
|
|
||||||
|
class CsrfTokenClient(
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val csrfTokenInterface: CsrfTokenInterface,
|
||||||
|
private val loginClient: LoginClient,
|
||||||
|
private val logoutClient: LogoutClient
|
||||||
|
) {
|
||||||
|
private var retries = 0
|
||||||
|
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun getTokenBlocking(): String {
|
||||||
|
var token = ""
|
||||||
|
val userName = sessionManager.userName ?: ""
|
||||||
|
val password = sessionManager.password ?: ""
|
||||||
|
|
||||||
|
for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) {
|
||||||
|
try {
|
||||||
|
if (retry > 0) {
|
||||||
|
// Log in explicitly
|
||||||
|
loginClient.loginBlocking(userName, password, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get CSRFToken response off the main thread.
|
||||||
|
val response = newSingleThreadExecutor().submit(Callable {
|
||||||
|
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||||
|
}).get()
|
||||||
|
|
||||||
|
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token = response.body()!!.query()!!.csrfToken()!!
|
||||||
|
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e: LoginFailedException) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
catch (t: Throwable) {
|
||||||
|
Timber.w(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.isEmpty() || token == ANON_TOKEN) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
|
||||||
|
requestToken(service, object : Callback {
|
||||||
|
override fun success(token: String?) {
|
||||||
|
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||||
|
retryWithLogin(cb) {
|
||||||
|
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.success(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||||
|
|
||||||
|
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||||
|
})
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
|
||||||
|
val call = service.getCsrfTokenCall()
|
||||||
|
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.success(response.body()!!.query()!!.csrfToken())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.failure(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return call
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
|
||||||
|
val userName = sessionManager.userName
|
||||||
|
val password = sessionManager.password
|
||||||
|
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||||
|
retries++
|
||||||
|
logoutClient.logout()
|
||||||
|
login(userName, password, callback) {
|
||||||
|
Timber.i("retrying...")
|
||||||
|
cancel()
|
||||||
|
csrfTokenCall = request(csrfTokenInterface, callback)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.failure(caught())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
callback: Callback,
|
||||||
|
retryCallback: () -> Unit
|
||||||
|
) = loginClient.request(username, password, object : LoginCallback {
|
||||||
|
override fun success(loginResult: LoginResult) {
|
||||||
|
if (loginResult.pass) {
|
||||||
|
sessionManager.updateAccount(loginResult)
|
||||||
|
retryCallback()
|
||||||
|
} else {
|
||||||
|
callback.failure(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun twoFactorPrompt(caught: Throwable, token: String?) =
|
||||||
|
callback.twoFactorPrompt()
|
||||||
|
|
||||||
|
// Should not happen here, but call the callback just in case.
|
||||||
|
override fun passwordResetPrompt(token: String?) =
|
||||||
|
callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||||
|
|
||||||
|
override fun error(caught: Throwable) = callback.failure(caught)
|
||||||
|
})
|
||||||
|
|
||||||
|
private fun cancel() {
|
||||||
|
loginClient.cancel()
|
||||||
|
if (csrfTokenCall != null) {
|
||||||
|
csrfTokenCall!!.cancel()
|
||||||
|
csrfTokenCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun success(token: String?)
|
||||||
|
fun failure(caught: Throwable?)
|
||||||
|
fun twoFactorPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ANON_TOKEN = "+\\"
|
||||||
|
private const val MAX_RETRIES = 1
|
||||||
|
private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2
|
||||||
|
const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure."
|
||||||
|
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class InvalidLoginTokenException(message: String) : Exception(message)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
|
||||||
|
interface CsrfTokenInterface {
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||||
|
fun getCsrfTokenCall(): Call<MwQueryResponse?>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) {
|
||||||
|
fun logout() = store.clear()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
interface LoginCallback {
|
||||||
|
fun success(loginResult: LoginResult)
|
||||||
|
fun twoFactorPrompt(caught: Throwable, token: String?)
|
||||||
|
fun passwordResetPrompt(token: String?)
|
||||||
|
fun error(caught: Throwable)
|
||||||
|
}
|
||||||
193
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
193
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for making login related requests to the server.
|
||||||
|
*/
|
||||||
|
class LoginClient(private val loginInterface: LoginInterface) {
|
||||||
|
private var tokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
private var loginCall: Call<LoginResponse?>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* userLanguage
|
||||||
|
* It holds the value of the user's device language code.
|
||||||
|
* For example, if user's device language is English it will hold En
|
||||||
|
* The value will be fetched when the user clicks Login Button in the LoginActivity
|
||||||
|
*/
|
||||||
|
private var userLanguage = ""
|
||||||
|
|
||||||
|
private fun getLoginToken() = loginInterface.getLoginToken()
|
||||||
|
|
||||||
|
fun request(userName: String, password: String, cb: LoginCallback) {
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
tokenCall = getLoginToken()
|
||||||
|
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||||
|
login(
|
||||||
|
userName, password, null, null, response.body()!!.query()!!.loginToken(),
|
||||||
|
userLanguage, cb
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(caught)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
|
||||||
|
loginToken: String?, userLanguage: String, cb: LoginCallback
|
||||||
|
) {
|
||||||
|
this.userLanguage = userLanguage
|
||||||
|
|
||||||
|
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||||
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
|
} else {
|
||||||
|
loginInterface.postLogIn(
|
||||||
|
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall!!.enqueue(object : Callback<LoginResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<LoginResponse?>,
|
||||||
|
response: Response<LoginResponse?>
|
||||||
|
) {
|
||||||
|
val loginResult = response.body()?.toLoginResult(password)
|
||||||
|
if (loginResult != null) {
|
||||||
|
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||||
|
// The server could do some transformations on user names, e.g. on some
|
||||||
|
// wikis is uppercases the first letter.
|
||||||
|
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||||
|
} else if ("UI" == loginResult.status) {
|
||||||
|
when (loginResult) {
|
||||||
|
is OAuthResult -> cb.twoFactorPrompt(
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken
|
||||||
|
)
|
||||||
|
|
||||||
|
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||||
|
|
||||||
|
is LoginResult.Result -> cb.error(
|
||||||
|
LoginFailedException(loginResult.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(IOException("Login failed. Unexpected response."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doLogin(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
twoFactorCode: String,
|
||||||
|
userLanguage: String,
|
||||||
|
loginCallback: LoginCallback
|
||||||
|
) {
|
||||||
|
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
response: Response<MwQueryResponse?>
|
||||||
|
) = if (response.isSuccessful){
|
||||||
|
val loginToken = response.body()?.query()?.loginToken()
|
||||||
|
loginToken?.let {
|
||||||
|
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
||||||
|
} ?: run {
|
||||||
|
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||||
|
loginCallback.error(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
|
||||||
|
val tokenResponse = getLoginToken().execute()
|
||||||
|
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
|
||||||
|
throw IOException("Unexpected response when getting login token.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||||
|
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
|
||||||
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
|
} else {
|
||||||
|
loginInterface.postLogIn(
|
||||||
|
userName, password, null, twoFactorCode, loginToken, userLanguage, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = tempLoginCall.execute()
|
||||||
|
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
|
||||||
|
if ("UI" == loginResult.status) {
|
||||||
|
if (loginResult is OAuthResult) {
|
||||||
|
// TODO: Find a better way to boil up the warning about 2FA
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) {
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) =
|
||||||
|
loginInterface.getUserInfo(userName)
|
||||||
|
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ response: MwQueryResponse? ->
|
||||||
|
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||||
|
loginResult.groups =
|
||||||
|
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
||||||
|
cb.success(loginResult)
|
||||||
|
}, { caught: Throwable ->
|
||||||
|
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||||
|
cb.error(caught)
|
||||||
|
})
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
tokenCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
tokenCall = null
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
loginCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
class LoginFailedException(message: String?) : Throwable(message)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface LoginInterface {
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=login")
|
||||||
|
fun getLoginToken(): Call<MwQueryResponse?>
|
||||||
|
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||||
|
fun postLogIn(
|
||||||
|
@Field("username") user: String?,
|
||||||
|
@Field("password") pass: String?,
|
||||||
|
@Field("logintoken") token: String?,
|
||||||
|
@Field("uselang") userLanguage: String?,
|
||||||
|
@Field("loginreturnurl") url: String?
|
||||||
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||||
|
fun postLogIn(
|
||||||
|
@Field("username") user: String?,
|
||||||
|
@Field("password") pass: String?,
|
||||||
|
@Field("retype") retypedPass: String?,
|
||||||
|
@Field("OATHToken") twoFactorCode: String?,
|
||||||
|
@Field("logintoken") token: String?,
|
||||||
|
@Field("uselang") userLanguage: String?,
|
||||||
|
@Field("logincontinue") loginContinue: Boolean
|
||||||
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
||||||
|
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
||||||
|
|
||||||
|
class LoginResponse {
|
||||||
|
@SerializedName("error")
|
||||||
|
val error: MwServiceError? = null
|
||||||
|
|
||||||
|
@SerializedName("clientlogin")
|
||||||
|
private val clientLogin: ClientLogin? = null
|
||||||
|
|
||||||
|
fun toLoginResult(password: String): LoginResult? {
|
||||||
|
return clientLogin?.toLoginResult(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClientLogin {
|
||||||
|
private val status: String? = null
|
||||||
|
private val requests: List<Request>? = null
|
||||||
|
private val message: String? = null
|
||||||
|
|
||||||
|
@SerializedName("username")
|
||||||
|
private val userName: String? = null
|
||||||
|
|
||||||
|
fun toLoginResult(password: String): LoginResult {
|
||||||
|
var userMessage = message
|
||||||
|
if ("UI" == status) {
|
||||||
|
if (requests != null) {
|
||||||
|
for (req in requests) {
|
||||||
|
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
|
||||||
|
return OAuthResult(status, userName, password, message)
|
||||||
|
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
|
||||||
|
return ResetPasswordResult(status, userName, password, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("PASS" != status && "FAIL" != status) {
|
||||||
|
//TODO: String resource -- Looks like needed for others in this class too
|
||||||
|
userMessage = "An unknown error occurred."
|
||||||
|
}
|
||||||
|
return Result(status ?: "", userName, password, userMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Request {
|
||||||
|
private val id: String? = null
|
||||||
|
private val required: String? = null
|
||||||
|
private val provider: String? = null
|
||||||
|
private val account: String? = null
|
||||||
|
private val fields: Map<String, RequestField>? = null
|
||||||
|
|
||||||
|
fun id(): String? = id
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RequestField {
|
||||||
|
private val type: String? = null
|
||||||
|
private val label: String? = null
|
||||||
|
private val help: String? = null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
sealed class LoginResult(
|
||||||
|
val status: String,
|
||||||
|
val userName: String?,
|
||||||
|
val password: String?,
|
||||||
|
val message: String?
|
||||||
|
) {
|
||||||
|
var userId = 0
|
||||||
|
var groups = emptySet<String>()
|
||||||
|
val pass: Boolean get() = "PASS" == status
|
||||||
|
|
||||||
|
class Result(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
): LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class OAuthResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class ResetPasswordResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
}
|
||||||
|
|
@ -5,23 +5,15 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.ParentViewPager;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionController;
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
|
@ -29,12 +21,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
private FragmentManager supportFragmentManager;
|
private FragmentManager supportFragmentManager;
|
||||||
private BookmarksPagerAdapter adapter;
|
private BookmarksPagerAdapter adapter;
|
||||||
@BindView(R.id.viewPagerBookmarks)
|
FragmentBookmarksBinding binding;
|
||||||
ParentViewPager viewPager;
|
|
||||||
@BindView(R.id.tab_layout)
|
|
||||||
TabLayout tabLayout;
|
|
||||||
@BindView(R.id.fragmentContainer)
|
|
||||||
FrameLayout fragmentContainer;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ContributionController controller;
|
ContributionController controller;
|
||||||
|
|
@ -54,7 +41,9 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScroll(boolean canScroll) {
|
public void setScroll(boolean canScroll) {
|
||||||
viewPager.setCanScroll(canScroll);
|
if (binding!=null) {
|
||||||
|
binding.viewPagerBookmarks.setCanScroll(canScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -68,8 +57,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreateView(inflater, container, savedInstanceState);
|
super.onCreateView(inflater, container, savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
binding = FragmentBookmarksBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
|
|
||||||
// Activity can call methods in the fragment by acquiring a
|
// Activity can call methods in the fragment by acquiring a
|
||||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||||
|
|
@ -77,14 +65,14 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
|
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
|
||||||
applicationKvStore.getBoolean("login_skipped"));
|
applicationKvStore.getBoolean("login_skipped"));
|
||||||
viewPager.setAdapter(adapter);
|
binding.viewPagerBookmarks.setAdapter(adapter);
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks);
|
||||||
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||||
|
|
||||||
setupTabLayout();
|
setupTabLayout();
|
||||||
return view;
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,15 +80,15 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
* visibility of tabLayout to gone.
|
* visibility of tabLayout to gone.
|
||||||
*/
|
*/
|
||||||
public void setupTabLayout() {
|
public void setupTabLayout() {
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
if (adapter.getCount() == 1) {
|
if (adapter.getCount() == 1) {
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.tabLayout.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition())))
|
if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition())))
|
||||||
.backPressed()) {
|
.backPressed()) {
|
||||||
// The event is handled internally by the adapter , no further action required.
|
// The event is handled internally by the adapter , no further action required.
|
||||||
return;
|
return;
|
||||||
|
|
@ -108,4 +96,10 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
||||||
// Event is not handled by the adapter ( performed back action ) change action bar.
|
// Event is not handled by the adapter ( performed back action ) change action bar.
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
||||||
|
|
@ -22,6 +20,7 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
import fr.free.nrw.commons.category.GridViewAdapter;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.navtab.NavTab;
|
import fr.free.nrw.commons.navtab.NavTab;
|
||||||
|
|
@ -39,8 +38,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
public Fragment listFragment;
|
public Fragment listFragment;
|
||||||
private BookmarksPagerAdapter bookmarksPagerAdapter;
|
private BookmarksPagerAdapter bookmarksPagerAdapter;
|
||||||
|
|
||||||
@BindView(R.id.explore_container)
|
FragmentFeaturedRootBinding binding;
|
||||||
FrameLayout container;
|
|
||||||
|
|
||||||
public BookmarkListRootFragment() {
|
public BookmarkListRootFragment() {
|
||||||
//empty constructor necessary otherwise crashes on recreate
|
//empty constructor necessary otherwise crashes on recreate
|
||||||
|
|
@ -70,9 +68,8 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, view);
|
return binding.getRoot();
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -184,7 +181,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
public void refreshNominatedMedia(int index) {
|
public void refreshNominatedMedia(int index) {
|
||||||
if (mediaDetails != null && !listFragment.isVisible()) {
|
if (mediaDetails != null && !listFragment.isVisible()) {
|
||||||
removeFragment(mediaDetails);
|
removeFragment(mediaDetails);
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
((BookmarkFragment) getParentFragment()).setScroll(false);
|
||||||
setFragment(mediaDetails, listFragment);
|
setFragment(mediaDetails, listFragment);
|
||||||
mediaDetails.showImage(index);
|
mediaDetails.showImage(index);
|
||||||
|
|
@ -241,9 +238,9 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
Log.d("deneme8", "on media clicked");
|
Log.d("deneme8", "on media clicked");
|
||||||
container.setVisibility(View.VISIBLE);
|
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||||
((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
((BookmarkFragment) getParentFragment()).setScroll(false);
|
||||||
setFragment(mediaDetails, listFragment);
|
setFragment(mediaDetails, listFragment);
|
||||||
mediaDetails.showImage(position);
|
mediaDetails.showImage(position);
|
||||||
|
|
@ -253,4 +250,10 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
||||||
public void onBackStackChanged() {
|
public void onBackStackChanged() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
||||||
|
|
||||||
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
|
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding;
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
@ -26,17 +25,7 @@ import org.jetbrains.annotations.NotNull;
|
||||||
*/
|
*/
|
||||||
public class BookmarkItemsFragment extends DaggerFragment {
|
public class BookmarkItemsFragment extends DaggerFragment {
|
||||||
|
|
||||||
@BindView(R.id.status_message)
|
private FragmentBookmarksItemsBinding binding;
|
||||||
TextView statusTextView;
|
|
||||||
|
|
||||||
@BindView(R.id.loading_images_progress_bar)
|
|
||||||
ProgressBar progressBar;
|
|
||||||
|
|
||||||
@BindView(R.id.list_view)
|
|
||||||
RecyclerView recyclerView;
|
|
||||||
|
|
||||||
@BindView(R.id.parent_layout)
|
|
||||||
RelativeLayout parentLayout;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BookmarkItemsController controller;
|
BookmarkItemsController controller;
|
||||||
|
|
@ -51,16 +40,13 @@ public class BookmarkItemsFragment extends DaggerFragment {
|
||||||
final ViewGroup container,
|
final ViewGroup container,
|
||||||
final Bundle savedInstanceState
|
final Bundle savedInstanceState
|
||||||
) {
|
) {
|
||||||
final View v = inflater.inflate(R.layout.fragment_bookmarks_items, container, false);
|
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, v);
|
return binding.getRoot();
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
|
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
initList(requireContext());
|
initList(requireContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,13 +63,19 @@ public class BookmarkItemsFragment extends DaggerFragment {
|
||||||
private void initList(final Context context) {
|
private void initList(final Context context) {
|
||||||
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
|
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
|
||||||
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
|
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
|
||||||
recyclerView.setAdapter(adapter);
|
binding.listView.setAdapter(adapter);
|
||||||
progressBar.setVisibility(View.GONE);
|
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
||||||
if (depictItems.isEmpty()) {
|
if (depictItems.isEmpty()) {
|
||||||
statusTextView.setText(R.string.bookmark_empty);
|
binding.statusMessage.setText(R.string.bookmark_empty);
|
||||||
statusTextView.setVisibility(View.VISIBLE);
|
binding.statusMessage.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
statusTextView.setVisibility(View.GONE);
|
binding.statusMessage.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,57 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations;
|
package fr.free.nrw.commons.bookmarks.locations;
|
||||||
|
|
||||||
|
import android.Manifest.permission;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ProgressBar;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
import android.widget.RelativeLayout;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import android.widget.TextView;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.ContributionController;
|
import fr.free.nrw.commons.contributions.ContributionController;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions;
|
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions;
|
||||||
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter;
|
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
|
||||||
public class BookmarkLocationsFragment extends DaggerFragment {
|
public class BookmarkLocationsFragment extends DaggerFragment {
|
||||||
|
|
||||||
@BindView(R.id.statusMessage) TextView statusTextView;
|
public FragmentBookmarksLocationsBinding binding;
|
||||||
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
|
|
||||||
@BindView(R.id.listView) RecyclerView recyclerView;
|
|
||||||
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
|
|
||||||
|
|
||||||
@Inject BookmarkLocationsController controller;
|
@Inject BookmarkLocationsController controller;
|
||||||
@Inject ContributionController contributionController;
|
@Inject ContributionController contributionController;
|
||||||
@Inject BookmarkLocationsDao bookmarkLocationDao;
|
@Inject BookmarkLocationsDao bookmarkLocationDao;
|
||||||
@Inject CommonPlaceClickActions commonPlaceClickActions;
|
@Inject CommonPlaceClickActions commonPlaceClickActions;
|
||||||
private PlaceAdapter adapter;
|
private PlaceAdapter adapter;
|
||||||
|
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(Map<String, Boolean> result) {
|
||||||
|
boolean areAllGranted = true;
|
||||||
|
for(final boolean b : result.values()) {
|
||||||
|
areAllGranted = areAllGranted && b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllGranted) {
|
||||||
|
contributionController.locationPermissionCallback.onLocationPermissionGranted();
|
||||||
|
} else {
|
||||||
|
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||||
|
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||||
|
} else {
|
||||||
|
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance of the fragment with the right bundle parameters
|
* Create an instance of the fragment with the right bundle parameters
|
||||||
|
|
@ -51,25 +67,25 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
||||||
ViewGroup container,
|
ViewGroup container,
|
||||||
Bundle savedInstanceState
|
Bundle savedInstanceState
|
||||||
) {
|
) {
|
||||||
View v = inflater.inflate(R.layout.fragment_bookmarks_locations, container, false);
|
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, v);
|
return binding.getRoot();
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
binding.loadingImagesProgressBar.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.listView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
adapter = new PlaceAdapter(bookmarkLocationDao,
|
adapter = new PlaceAdapter(bookmarkLocationDao,
|
||||||
place -> Unit.INSTANCE,
|
place -> Unit.INSTANCE,
|
||||||
(place, isBookmarked) -> {
|
(place, isBookmarked) -> {
|
||||||
adapter.remove(place);
|
adapter.remove(place);
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
},
|
},
|
||||||
commonPlaceClickActions
|
commonPlaceClickActions,
|
||||||
|
inAppCameraLocationPermissionLauncher
|
||||||
);
|
);
|
||||||
recyclerView.setAdapter(adapter);
|
binding.listView.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -84,12 +100,12 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
||||||
private void initList() {
|
private void initList() {
|
||||||
List<Place> places = controller.loadFavoritesLocations();
|
List<Place> places = controller.loadFavoritesLocations();
|
||||||
adapter.setItems(places);
|
adapter.setItems(places);
|
||||||
progressBar.setVisibility(View.GONE);
|
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
||||||
if (places.size() <= 0) {
|
if (places.size() <= 0) {
|
||||||
statusTextView.setText(R.string.bookmark_empty);
|
binding.statusMessage.setText(R.string.bookmark_empty);
|
||||||
statusTextView.setVisibility(View.VISIBLE);
|
binding.statusMessage.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
statusTextView.setVisibility(View.GONE);
|
binding.statusMessage.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,4 +113,10 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data);
|
contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,15 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.GridView;
|
|
||||||
import android.widget.ListAdapter;
|
import android.widget.ListAdapter;
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
import dagger.android.support.DaggerFragment;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
|
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
|
||||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
import fr.free.nrw.commons.category.GridViewAdapter;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding;
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
@ -37,11 +32,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
private GridViewAdapter gridAdapter;
|
private GridViewAdapter gridAdapter;
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
@BindView(R.id.statusMessage) TextView statusTextView;
|
private FragmentBookmarksPicturesBinding binding;
|
||||||
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
|
|
||||||
@BindView(R.id.bookmarkedPicturesList) GridView gridView;
|
|
||||||
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BookmarkPicturesController controller;
|
BookmarkPicturesController controller;
|
||||||
|
|
||||||
|
|
@ -59,15 +50,14 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
ViewGroup container,
|
ViewGroup container,
|
||||||
Bundle savedInstanceState
|
Bundle savedInstanceState
|
||||||
) {
|
) {
|
||||||
View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false);
|
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, v);
|
return binding.getRoot();
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
|
binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
|
||||||
initList();
|
initList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,13 +71,14 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
compositeDisposable.clear();
|
compositeDisposable.clear();
|
||||||
|
binding = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
if (controller.needRefreshBookmarkedPictures()) {
|
if (controller.needRefreshBookmarkedPictures()) {
|
||||||
gridView.setVisibility(GONE);
|
binding.bookmarkedPicturesList.setVisibility(GONE);
|
||||||
if (gridAdapter != null) {
|
if (gridAdapter != null) {
|
||||||
gridAdapter.clear();
|
gridAdapter.clear();
|
||||||
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
|
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
|
||||||
|
|
@ -107,8 +98,8 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.setVisibility(VISIBLE);
|
binding.loadingImagesProgressBar.setVisibility(VISIBLE);
|
||||||
statusTextView.setVisibility(GONE);
|
binding.statusMessage.setVisibility(GONE);
|
||||||
|
|
||||||
compositeDisposable.add(controller.loadBookmarkedPictures()
|
compositeDisposable.add(controller.loadBookmarkedPictures()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
@ -120,12 +111,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
* Handles the UI updates for no internet scenario
|
* Handles the UI updates for no internet scenario
|
||||||
*/
|
*/
|
||||||
private void handleNoInternet() {
|
private void handleNoInternet() {
|
||||||
progressBar.setVisibility(GONE);
|
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||||
statusTextView.setVisibility(VISIBLE);
|
binding.statusMessage.setVisibility(VISIBLE);
|
||||||
statusTextView.setText(getString(R.string.no_internet));
|
binding.statusMessage.setText(getString(R.string.no_internet));
|
||||||
} else {
|
} else {
|
||||||
ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
|
ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +127,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
private void handleError(Throwable throwable) {
|
private void handleError(Throwable throwable) {
|
||||||
Timber.e(throwable, "Error occurred while loading images inside a category");
|
Timber.e(throwable, "Error occurred while loading images inside a category");
|
||||||
try{
|
try{
|
||||||
ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
|
ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images);
|
||||||
initErrorView();
|
initErrorView();
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
@ -147,12 +138,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
* Handles the UI updates for a error scenario
|
* Handles the UI updates for a error scenario
|
||||||
*/
|
*/
|
||||||
private void initErrorView() {
|
private void initErrorView() {
|
||||||
progressBar.setVisibility(GONE);
|
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||||
statusTextView.setVisibility(VISIBLE);
|
binding.statusMessage.setVisibility(VISIBLE);
|
||||||
statusTextView.setText(getString(R.string.no_images_found));
|
binding.statusMessage.setText(getString(R.string.no_images_found));
|
||||||
} else {
|
} else {
|
||||||
statusTextView.setVisibility(GONE);
|
binding.statusMessage.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,12 +151,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
* Handles the UI updates when there is no bookmarks
|
* Handles the UI updates when there is no bookmarks
|
||||||
*/
|
*/
|
||||||
private void initEmptyBookmarkListView() {
|
private void initEmptyBookmarkListView() {
|
||||||
progressBar.setVisibility(GONE);
|
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||||
statusTextView.setVisibility(VISIBLE);
|
binding.statusMessage.setVisibility(VISIBLE);
|
||||||
statusTextView.setText(getString(R.string.bookmark_empty));
|
binding.statusMessage.setText(getString(R.string.bookmark_empty));
|
||||||
} else {
|
} else {
|
||||||
statusTextView.setVisibility(GONE);
|
binding.statusMessage.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,18 +179,18 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
setAdapter(collection);
|
setAdapter(collection);
|
||||||
} else {
|
} else {
|
||||||
if (gridAdapter.containsAll(collection)) {
|
if (gridAdapter.containsAll(collection)) {
|
||||||
progressBar.setVisibility(GONE);
|
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||||
statusTextView.setVisibility(GONE);
|
binding.statusMessage.setVisibility(GONE);
|
||||||
gridView.setVisibility(VISIBLE);
|
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
||||||
gridView.setAdapter(gridAdapter);
|
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gridAdapter.addItems(collection);
|
gridAdapter.addItems(collection);
|
||||||
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
|
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
|
||||||
}
|
}
|
||||||
progressBar.setVisibility(GONE);
|
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||||
statusTextView.setVisibility(GONE);
|
binding.statusMessage.setVisibility(GONE);
|
||||||
gridView.setVisibility(VISIBLE);
|
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -212,7 +203,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
R.layout.layout_category_images,
|
R.layout.layout_category_images,
|
||||||
mediaList
|
mediaList
|
||||||
);
|
);
|
||||||
gridView.setAdapter(gridAdapter);
|
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -221,6 +212,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
||||||
* @return GridView Adapter
|
* @return GridView Adapter
|
||||||
*/
|
*/
|
||||||
public ListAdapter getAdapter() {
|
public ListAdapter getAdapter() {
|
||||||
return gridView.getAdapter();
|
return binding.bookmarkedPicturesList.getAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,20 @@ package fr.free.nrw.commons.campaigns;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||||
|
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import org.wikipedia.util.DateUtil;
|
import fr.free.nrw.commons.utils.DateUtil;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
|
@ -31,6 +29,7 @@ import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
*/
|
*/
|
||||||
public class CampaignView extends SwipableCardView {
|
public class CampaignView extends SwipableCardView {
|
||||||
Campaign campaign;
|
Campaign campaign;
|
||||||
|
private LayoutCampaginBinding binding;
|
||||||
private ViewHolder viewHolder;
|
private ViewHolder viewHolder;
|
||||||
|
|
||||||
public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView";
|
public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView";
|
||||||
|
|
@ -69,15 +68,15 @@ public class CampaignView extends SwipableCardView {
|
||||||
@Override public boolean onSwipe(final View view) {
|
@Override public boolean onSwipe(final View view) {
|
||||||
view.setVisibility(View.GONE);
|
view.setVisibility(View.GONE);
|
||||||
((BaseActivity) getContext()).defaultKvStore
|
((BaseActivity) getContext()).defaultKvStore
|
||||||
.putBoolean(campaignPreference, false);
|
.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false);
|
||||||
ViewUtil.showLongToast(getContext(),
|
ViewUtil.showLongToast(getContext(),
|
||||||
getResources().getString(R.string.nearby_campaign_dismiss_message));
|
getResources().getString(R.string.nearby_campaign_dismiss_message));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
final View rootView = inflate(getContext(), R.layout.layout_campagin, this);
|
binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true);
|
||||||
viewHolder = new ViewHolder(rootView);
|
viewHolder = new ViewHolder();
|
||||||
setOnClickListener(view -> {
|
setOnClickListener(view -> {
|
||||||
if (campaign != null) {
|
if (campaign != null) {
|
||||||
if (campaign.isWLMCampaign()) {
|
if (campaign.isWLMCampaign()) {
|
||||||
|
|
@ -90,27 +89,16 @@ public class CampaignView extends SwipableCardView {
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ViewHolder {
|
public class ViewHolder {
|
||||||
|
|
||||||
@BindView(R.id.iv_campaign)
|
|
||||||
ImageView ivCampaign;
|
|
||||||
@BindView(R.id.tv_title) TextView tvTitle;
|
|
||||||
@BindView(R.id.tv_description) TextView tvDescription;
|
|
||||||
@BindView(R.id.tv_dates) TextView tvDates;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
ButterKnife.bind(this, itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
if (campaign != null) {
|
if (campaign != null) {
|
||||||
ivCampaign.setImageDrawable(
|
binding.ivCampaign.setImageDrawable(
|
||||||
getResources().getDrawable(R.drawable.ic_campaign));
|
getResources().getDrawable(R.drawable.ic_campaign));
|
||||||
|
|
||||||
tvTitle.setText(campaign.getTitle());
|
binding.tvTitle.setText(campaign.getTitle());
|
||||||
tvDescription.setText(campaign.getDescription());
|
binding.tvDescription.setText(campaign.getDescription());
|
||||||
try {
|
try {
|
||||||
if (campaign.isWLMCampaign()) {
|
if (campaign.isWLMCampaign()) {
|
||||||
tvDates.setText(
|
binding.tvDates.setText(
|
||||||
String.format("%1s - %2s", campaign.getStartDate(),
|
String.format("%1s - %2s", campaign.getStartDate(),
|
||||||
campaign.getEndDate()));
|
campaign.getEndDate()));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -118,7 +106,7 @@ public class CampaignView extends SwipableCardView {
|
||||||
.parse(campaign.getStartDate());
|
.parse(campaign.getStartDate());
|
||||||
final Date endDate = CommonsDateUtil.getIso8601DateFormatShort()
|
final Date endDate = CommonsDateUtil.getIso8601DateFormatShort()
|
||||||
.parse(campaign.getEndDate());
|
.parse(campaign.getEndDate());
|
||||||
tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
|
binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
|
||||||
DateUtil.getExtraShortDateString(endDate)));
|
DateUtil.getExtraShortDateString(endDate)));
|
||||||
}
|
}
|
||||||
} catch (final ParseException e) {
|
} catch (final ParseException e) {
|
||||||
|
|
|
||||||
|
|
@ -27,30 +27,42 @@ class CategoriesModel @Inject constructor(
|
||||||
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the item contains an year
|
* Returns true if an item is considered to be a spammy category which should be ignored
|
||||||
* @param item
|
*
|
||||||
|
* @param item a category item that needs to be validated to know if it is spammy or not
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
fun containsYear(item: String): Boolean {
|
fun isSpammyCategory(item: String): Boolean {
|
||||||
//Check for current and previous year to exclude these categories from removal
|
//Check for current and previous year to exclude these categories from removal
|
||||||
val now = Calendar.getInstance()
|
val now = Calendar.getInstance()
|
||||||
val year = now[Calendar.YEAR]
|
val curYear = now[Calendar.YEAR]
|
||||||
val yearInString = year.toString()
|
val curYearInString = curYear.toString()
|
||||||
val prevYear = year - 1
|
val prevYear = curYear - 1
|
||||||
val prevYearInString = prevYear.toString()
|
val prevYearInString = prevYear.toString()
|
||||||
Timber.d("Previous year: %s", prevYearInString)
|
Timber.d("Previous year: %s", prevYearInString)
|
||||||
|
|
||||||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
||||||
//And that item does not equal the current year or previous year
|
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
||||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
val spammyCategory = item.matches("(.*)needing(.*)".toRegex())
|
||||||
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
|
||||||
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
|
||||||
&& !item.contains(yearInString)
|
|
||||||
&& !item.contains(prevYearInString)
|
|
||||||
|| item.matches("(.*)needing(.*)".toRegex())
|
|
||||||
|| item.matches("(.*)taken on(.*)".toRegex())
|
|| item.matches("(.*)taken on(.*)".toRegex())
|
||||||
|| item.matches(".*0s.*".toRegex())
|
|
||||||
&& !item.matches(".*(200|201)0s.*".toRegex())
|
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
|
if (spammyCategory) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentionsDecade) {
|
||||||
|
// Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029
|
||||||
|
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
||||||
|
return !recentDecade
|
||||||
|
} else {
|
||||||
|
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
||||||
|
// anywhere within the string (.* is wildcard) (Issue #47)
|
||||||
|
// And that item does not equal the current year or previous year
|
||||||
|
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
||||||
|
&& !item.contains(curYearInString)
|
||||||
|
&& !item.contains(prevYearInString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -136,7 +148,11 @@ class CategoriesModel @Inject constructor(
|
||||||
return Observable.fromIterable(categoryNames)
|
return Observable.fromIterable(categoryNames)
|
||||||
.map { categoryName ->
|
.map { categoryName ->
|
||||||
buildCategories(categoryName)
|
buildCategories(categoryName)
|
||||||
}.toList().toObservable()
|
}
|
||||||
|
.filter { categoryItem ->
|
||||||
|
categoryItem.name != "Hidden"
|
||||||
|
}
|
||||||
|
.toList().toObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package fr.free.nrw.commons.category
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,12 @@ import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding;
|
||||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
|
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
|
||||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
|
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
|
||||||
|
|
@ -29,7 +28,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.wikipedia.page.PageTitle;
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This activity displays details of a particular category
|
* This activity displays details of a particular category
|
||||||
|
|
@ -45,23 +44,23 @@ public class CategoryDetailsActivity extends BaseActivity
|
||||||
private CategoriesMediaFragment categoriesMediaFragment;
|
private CategoriesMediaFragment categoriesMediaFragment;
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
private String categoryName;
|
private String categoryName;
|
||||||
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
|
|
||||||
@BindView(R.id.tab_layout) TabLayout tabLayout;
|
|
||||||
@BindView(R.id.viewPager) ViewPager viewPager;
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
|
||||||
ViewPagerAdapter viewPagerAdapter;
|
ViewPagerAdapter viewPagerAdapter;
|
||||||
|
|
||||||
|
private ActivityCategoryDetailsBinding binding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_category_details);
|
|
||||||
ButterKnife.bind(this);
|
binding = ActivityCategoryDetailsBinding.inflate(getLayoutInflater());
|
||||||
|
final View view = binding.getRoot();
|
||||||
|
setContentView(view);
|
||||||
supportFragmentManager = getSupportFragmentManager();
|
supportFragmentManager = getSupportFragmentManager();
|
||||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||||
viewPager.setAdapter(viewPagerAdapter);
|
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||||
viewPager.setOffscreenPageLimit(2);
|
binding.viewPager.setOffscreenPageLimit(2);
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
setTabs();
|
setTabs();
|
||||||
setPageTitle();
|
setPageTitle();
|
||||||
|
|
@ -110,12 +109,12 @@ public class CategoryDetailsActivity extends BaseActivity
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int position) {
|
public void onMediaClicked(int position) {
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.tabLayout.setVisibility(View.GONE);
|
||||||
viewPager.setVisibility(View.GONE);
|
binding.viewPager.setVisibility(View.GONE);
|
||||||
mediaContainer.setVisibility(View.VISIBLE);
|
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
|
|
@ -216,9 +215,9 @@ public class CategoryDetailsActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
viewPager.setVisibility(View.VISIBLE);
|
binding.viewPager.setVisibility(View.VISIBLE);
|
||||||
mediaContainer.setVisibility(View.GONE);
|
binding.mediaContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_E
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
package fr.free.nrw.commons.category;
|
|
||||||
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import retrofit2.http.GET;
|
|
||||||
import retrofit2.http.Query;
|
|
||||||
import retrofit2.http.QueryMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for interacting with Commons category related APIs
|
|
||||||
*/
|
|
||||||
public interface CategoryInterface {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for categories with the specified name.
|
|
||||||
*
|
|
||||||
* @param filter The string to be searched
|
|
||||||
* @param itemLimit How many results are returned
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
|
||||||
+ "&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70"
|
|
||||||
+ "&gsrnamespace=14")
|
|
||||||
Single<MwQueryResponse> searchCategories(@Query("gsrsearch") String filter,
|
|
||||||
@Query("gsrlimit") int itemLimit,
|
|
||||||
@Query("gsroffset") int offset);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for categories starting with the specified prefix.
|
|
||||||
*
|
|
||||||
* @param prefix The string to be searched
|
|
||||||
* @param itemLimit How many results are returned
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
|
||||||
+ "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail"
|
|
||||||
+ "&pithumbsize=70")
|
|
||||||
Single<MwQueryResponse> searchCategoriesForPrefix(@Query("gacprefix") String prefix,
|
|
||||||
@Query("gaclimit") int itemLimit,
|
|
||||||
@Query("gacoffset") int offset);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches categories starting and ending with a specified name.
|
|
||||||
*
|
|
||||||
* @param startingCategory Name of the category to start
|
|
||||||
* @param endingCategory Name of the category to end
|
|
||||||
* @param itemLimit How many categories to return
|
|
||||||
* @param offset offset
|
|
||||||
* @return MwQueryResponse
|
|
||||||
*/
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
|
||||||
+ "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail"
|
|
||||||
+ "&pithumbsize=70")
|
|
||||||
Single<MwQueryResponse> getCategoriesByName(@Query("gacfrom") String startingCategory,
|
|
||||||
@Query("gacto") String endingCategory,
|
|
||||||
@Query("gaclimit") int itemLimit,
|
|
||||||
@Query("gacoffset") int offset);
|
|
||||||
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
|
||||||
+ "&generator=categorymembers&gcmtype=subcat"
|
|
||||||
+ "&prop=info&gcmlimit=50")
|
|
||||||
Single<MwQueryResponse> getSubCategoryList(@Query("gcmtitle") String categoryName,
|
|
||||||
@QueryMap(encoded = true) Map<String, String> continuation);
|
|
||||||
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
|
||||||
+ "&generator=categories&prop=info&gcllimit=50")
|
|
||||||
Single<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName,
|
|
||||||
@QueryMap(encoded = true) Map<String, String> continuation);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import io.reactivex.Single
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Query
|
||||||
|
import retrofit2.http.QueryMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for interacting with Commons category related APIs
|
||||||
|
*/
|
||||||
|
interface CategoryInterface {
|
||||||
|
/**
|
||||||
|
* Searches for categories with the specified name.
|
||||||
|
*
|
||||||
|
* @param filter The string to be searched
|
||||||
|
* @param itemLimit How many results are returned
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14")
|
||||||
|
fun searchCategories(
|
||||||
|
@Query("gsrsearch") filter: String?,
|
||||||
|
@Query("gsrlimit") itemLimit: Int,
|
||||||
|
@Query("gsroffset") offset: Int
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for categories starting with the specified prefix.
|
||||||
|
*
|
||||||
|
* @param prefix The string to be searched
|
||||||
|
* @param itemLimit How many results are returned
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||||
|
fun searchCategoriesForPrefix(
|
||||||
|
@Query("gacprefix") prefix: String?,
|
||||||
|
@Query("gaclimit") itemLimit: Int,
|
||||||
|
@Query("gacoffset") offset: Int
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches categories starting and ending with a specified name.
|
||||||
|
*
|
||||||
|
* @param startingCategory Name of the category to start
|
||||||
|
* @param endingCategory Name of the category to end
|
||||||
|
* @param itemLimit How many categories to return
|
||||||
|
* @param offset offset
|
||||||
|
* @return MwQueryResponse
|
||||||
|
*/
|
||||||
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||||
|
fun getCategoriesByName(
|
||||||
|
@Query("gacfrom") startingCategory: String?,
|
||||||
|
@Query("gacto") endingCategory: String?,
|
||||||
|
@Query("gaclimit") itemLimit: Int,
|
||||||
|
@Query("gacoffset") offset: Int
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||||
|
fun getSubCategoryList(
|
||||||
|
@Query("gcmtitle") categoryName: String,
|
||||||
|
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
|
||||||
|
fun getParentCategoryList(
|
||||||
|
@Query("titles") categoryName: String?,
|
||||||
|
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package fr.free.nrw.commons.category
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CategoryItem(val name: String, val description: String?,
|
data class CategoryItem(val name: String, val description: String?,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.os.Parcelable
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.upload.UploadItem
|
import fr.free.nrw.commons.upload.UploadItem
|
||||||
|
|
@ -12,8 +13,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||||
import fr.free.nrw.commons.upload.WikidataPlace
|
import fr.free.nrw.commons.upload.WikidataPlace
|
||||||
import fr.free.nrw.commons.upload.WikidataPlace.Companion.from
|
import fr.free.nrw.commons.upload.WikidataPlace.Companion.from
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(tableName = "contribution")
|
@Entity(tableName = "contribution")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -43,7 +45,11 @@ data class Contribution constructor(
|
||||||
var hasInvalidLocation : Int = 0,
|
var hasInvalidLocation : Int = 0,
|
||||||
var contentUri: Uri? = null,
|
var contentUri: Uri? = null,
|
||||||
var countryCode : String? = null,
|
var countryCode : String? = null,
|
||||||
var imageSHA1 : String? = null
|
var imageSHA1 : String? = null,
|
||||||
|
/**
|
||||||
|
* Number of times a contribution has been retried after a failure
|
||||||
|
*/
|
||||||
|
var retries: Int = 0
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun completeWith(media: Media): Contribution {
|
fun completeWith(media: Media): Contribution {
|
||||||
|
|
@ -111,6 +117,21 @@ data class Contribution constructor(
|
||||||
*/
|
*/
|
||||||
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
||||||
descriptions.filter { it.descriptionText.isNotEmpty() }
|
descriptions.filter { it.descriptionText.isNotEmpty() }
|
||||||
.joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileKey : String? get() = chunkInfo?.uploadResult?.filekey
|
||||||
|
val localUriPath: File? get() = localUri?.path?.let { File(it) }
|
||||||
|
|
||||||
|
fun isCompleted(): Boolean {
|
||||||
|
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPaused(): Boolean {
|
||||||
|
return CommonsApplication.pauseUploads[pageId] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpause() {
|
||||||
|
CommonsApplication.pauseUploads[pageId] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build.VERSION;
|
import android.widget.Toast;
|
||||||
import android.os.Build.VERSION_CODES;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.filepicker.DefaultCallback;
|
import fr.free.nrw.commons.filepicker.DefaultCallback;
|
||||||
|
|
@ -16,8 +15,13 @@ import fr.free.nrw.commons.filepicker.FilePicker;
|
||||||
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
|
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
|
||||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||||
|
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
import fr.free.nrw.commons.upload.UploadActivity;
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -31,6 +35,13 @@ public class ContributionController {
|
||||||
|
|
||||||
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
|
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
|
||||||
private final JsonKvStore defaultKvStore;
|
private final JsonKvStore defaultKvStore;
|
||||||
|
private LatLng locationBeforeImageCapture;
|
||||||
|
private boolean isInAppCameraUpload;
|
||||||
|
public LocationPermissionCallback locationPermissionCallback;
|
||||||
|
private LocationPermissionsHelper locationPermissionsHelper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
LocationServiceManager locationManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
|
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
|
||||||
|
|
@ -40,7 +51,8 @@ public class ContributionController {
|
||||||
/**
|
/**
|
||||||
* Check for permissions and initiate camera click
|
* Check for permissions and initiate camera click
|
||||||
*/
|
*/
|
||||||
public void initiateCameraPick(Activity activity) {
|
public void initiateCameraPick(Activity activity,
|
||||||
|
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||||
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
||||||
if (!useExtStorage) {
|
if (!useExtStorage) {
|
||||||
initiateCameraUpload(activity);
|
initiateCameraUpload(activity);
|
||||||
|
|
@ -48,10 +60,133 @@ public class ContributionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
() -> {
|
||||||
() -> initiateCameraUpload(activity),
|
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
|
||||||
|
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
|
||||||
|
askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher);
|
||||||
|
} else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
|
||||||
|
createDialogsAndHandleLocationPermissions(activity,
|
||||||
|
inAppCameraLocationPermissionLauncher);
|
||||||
|
} else {
|
||||||
|
initiateCameraUpload(activity);
|
||||||
|
}
|
||||||
|
},
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale);
|
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,33 +200,25 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -110,14 +237,20 @@ 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) {
|
||||||
|
|
@ -126,12 +259,14 @@ public class ContributionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) {
|
public void onImagePickerError(Exception e, FilePicker.ImageSource source,
|
||||||
|
int type) {
|
||||||
ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images);
|
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,
|
||||||
|
FilePicker.ImageSource source, int type) {
|
||||||
Intent intent = handleImagesPicked(activity, imagesFiles);
|
Intent intent = handleImagesPicked(activity, imagesFiles);
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
@ -144,8 +279,8 @@ public class ContributionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return shareIntent;
|
if (locationBeforeImageCapture != null) {
|
||||||
|
shareIntent.putExtra(
|
||||||
|
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
|
||||||
|
locationBeforeImageCapture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shareIntent.putExtra(
|
||||||
|
UploadActivity.IN_APP_CAMERA_UPLOAD,
|
||||||
|
isInAppCameraUpload
|
||||||
|
);
|
||||||
|
isInAppCameraUpload = false; // reset the flag for next use
|
||||||
|
return shareIntent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.room.Update;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,12 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AlertDialog.Builder;
|
import androidx.appcompat.app.AlertDialog.Builder;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
import com.facebook.imagepipeline.request.ImageRequest;
|
import com.facebook.imagepipeline.request.ImageRequest;
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
||||||
|
import fr.free.nrw.commons.databinding.LayoutContributionBinding;
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
@ -29,29 +27,8 @@ import java.io.File;
|
||||||
public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
@BindView(R.id.contributionImage)
|
|
||||||
SimpleDraweeView imageView;
|
|
||||||
@BindView(R.id.contributionTitle)
|
|
||||||
TextView titleView;
|
|
||||||
@BindView(R.id.authorView)
|
|
||||||
TextView authorView;
|
|
||||||
@BindView(R.id.contributionState)
|
|
||||||
TextView stateView;
|
|
||||||
@BindView(R.id.contributionSequenceNumber)
|
|
||||||
TextView seqNumView;
|
|
||||||
@BindView(R.id.contributionProgress)
|
|
||||||
ProgressBar progressView;
|
|
||||||
@BindView(R.id.image_options)
|
|
||||||
RelativeLayout imageOptions;
|
|
||||||
@BindView(R.id.wikipediaButton)
|
|
||||||
ImageButton addToWikipediaButton;
|
|
||||||
@BindView(R.id.retryButton)
|
|
||||||
ImageButton retryButton;
|
|
||||||
@BindView(R.id.cancelButton)
|
|
||||||
ImageButton cancelButton;
|
|
||||||
@BindView(R.id.pauseResumeButton)
|
|
||||||
ImageButton pauseResumeButton;
|
|
||||||
|
|
||||||
|
LayoutContributionBinding binding;
|
||||||
|
|
||||||
private int position;
|
private int position;
|
||||||
private Contribution contribution;
|
private Contribution contribution;
|
||||||
|
|
@ -67,9 +44,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
super(parent);
|
super(parent);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.mediaClient = mediaClient;
|
this.mediaClient = mediaClient;
|
||||||
ButterKnife.bind(this, parent);
|
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
||||||
|
binding = LayoutContributionBinding.bind(parent);
|
||||||
|
|
||||||
|
binding.retryButton.setOnClickListener(v -> retryUpload());
|
||||||
|
binding.cancelButton.setOnClickListener(v -> deleteUpload());
|
||||||
|
binding.contributionImage.setOnClickListener(v -> imageClicked());
|
||||||
|
binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
|
||||||
|
binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked());
|
||||||
|
|
||||||
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
|
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
|
||||||
an upload might take a dozen seconds. */
|
an upload might take a dozen seconds. */
|
||||||
AlertDialog.Builder builder = new Builder(parent.getContext());
|
AlertDialog.Builder builder = new Builder(parent.getContext());
|
||||||
|
|
@ -87,14 +71,17 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
this.contribution = contribution;
|
this.contribution = contribution;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
titleView.setText(contribution.getMedia().getMostRelevantCaption());
|
binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption());
|
||||||
authorView.setText(contribution.getMedia().getAuthor());
|
binding.authorView.setText(contribution.getMedia().getAuthor());
|
||||||
|
|
||||||
//Removes flicker of loading image.
|
//Removes flicker of loading image.
|
||||||
imageView.getHierarchy().setFadeDuration(0);
|
binding.contributionImage.getHierarchy().setFadeDuration(0);
|
||||||
|
|
||||||
|
binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
|
||||||
|
binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
|
|
||||||
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
|
|
||||||
|
|
||||||
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
|
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
|
||||||
contribution.getLocalUri());
|
contribution.getLocalUri());
|
||||||
|
|
@ -103,73 +90,77 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
||||||
.setProgressiveRenderingEnabled(true)
|
.setProgressiveRenderingEnabled(true)
|
||||||
.build();
|
.build();
|
||||||
} else if(imageSource != null) {
|
}
|
||||||
|
else if (URLUtil.isFileUrl(imageSource)){
|
||||||
|
imageRequest=ImageRequest.fromUri(Uri.parse(imageSource));
|
||||||
|
}
|
||||||
|
else if(imageSource != null) {
|
||||||
final File file = new File(imageSource);
|
final File file = new File(imageSource);
|
||||||
imageRequest = ImageRequest.fromFile(file);
|
imageRequest = ImageRequest.fromFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(imageRequest != null){
|
if(imageRequest != null){
|
||||||
imageView.setImageRequest(imageRequest);
|
binding.contributionImage.setImageRequest(imageRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seqNumView.setText(String.valueOf(position + 1));
|
binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
|
||||||
seqNumView.setVisibility(View.VISIBLE);
|
binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
addToWikipediaButton.setVisibility(View.GONE);
|
binding.wikipediaButton.setVisibility(View.GONE);
|
||||||
switch (contribution.getState()) {
|
switch (contribution.getState()) {
|
||||||
case Contribution.STATE_COMPLETED:
|
case Contribution.STATE_COMPLETED:
|
||||||
stateView.setVisibility(View.GONE);
|
binding.contributionState.setVisibility(View.GONE);
|
||||||
progressView.setVisibility(View.GONE);
|
binding.contributionProgress.setVisibility(View.GONE);
|
||||||
imageOptions.setVisibility(View.GONE);
|
binding.imageOptions.setVisibility(View.GONE);
|
||||||
stateView.setText("");
|
binding.contributionState.setText("");
|
||||||
checkIfMediaExistsOnWikipediaPage(contribution);
|
checkIfMediaExistsOnWikipediaPage(contribution);
|
||||||
break;
|
break;
|
||||||
case Contribution.STATE_QUEUED:
|
case Contribution.STATE_QUEUED:
|
||||||
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
|
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
|
||||||
progressView.setVisibility(View.GONE);
|
binding.contributionProgress.setVisibility(View.GONE);
|
||||||
stateView.setVisibility(View.VISIBLE);
|
binding.contributionState.setVisibility(View.VISIBLE);
|
||||||
stateView.setText(R.string.contribution_state_queued);
|
binding.contributionState.setText(R.string.contribution_state_queued);
|
||||||
imageOptions.setVisibility(View.GONE);
|
binding.imageOptions.setVisibility(View.GONE);
|
||||||
break;
|
break;
|
||||||
case Contribution.STATE_IN_PROGRESS:
|
case Contribution.STATE_IN_PROGRESS:
|
||||||
stateView.setVisibility(View.GONE);
|
binding.contributionState.setVisibility(View.GONE);
|
||||||
progressView.setVisibility(View.VISIBLE);
|
binding.contributionProgress.setVisibility(View.VISIBLE);
|
||||||
addToWikipediaButton.setVisibility(View.GONE);
|
binding.wikipediaButton.setVisibility(View.GONE);
|
||||||
pauseResumeButton.setVisibility(View.VISIBLE);
|
binding.pauseResumeButton.setVisibility(View.VISIBLE);
|
||||||
cancelButton.setVisibility(View.GONE);
|
binding.cancelButton.setVisibility(View.GONE);
|
||||||
retryButton.setVisibility(View.GONE);
|
binding.retryButton.setVisibility(View.GONE);
|
||||||
imageOptions.setVisibility(View.VISIBLE);
|
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||||
final long total = contribution.getDataLength();
|
final long total = contribution.getDataLength();
|
||||||
final long transferred = contribution.getTransferred();
|
final long transferred = contribution.getTransferred();
|
||||||
if (transferred == 0 || transferred >= total) {
|
if (transferred == 0 || transferred >= total) {
|
||||||
progressView.setIndeterminate(true);
|
binding.contributionProgress.setIndeterminate(true);
|
||||||
} else {
|
} else {
|
||||||
progressView.setIndeterminate(false);
|
binding.contributionProgress.setIndeterminate(false);
|
||||||
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
|
binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Contribution.STATE_PAUSED:
|
case Contribution.STATE_PAUSED:
|
||||||
progressView.setVisibility(View.GONE);
|
binding.contributionProgress.setVisibility(View.GONE);
|
||||||
stateView.setVisibility(View.VISIBLE);
|
binding.contributionState.setVisibility(View.VISIBLE);
|
||||||
stateView.setText(R.string.paused);
|
binding.contributionState.setText(R.string.paused);
|
||||||
cancelButton.setVisibility(View.VISIBLE);
|
binding.cancelButton.setVisibility(View.VISIBLE);
|
||||||
retryButton.setVisibility(View.GONE);
|
binding.retryButton.setVisibility(View.GONE);
|
||||||
pauseResumeButton.setVisibility(View.VISIBLE);
|
binding.pauseResumeButton.setVisibility(View.VISIBLE);
|
||||||
imageOptions.setVisibility(View.VISIBLE);
|
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||||
setResume();
|
setResume();
|
||||||
if(pausingPopUp.isShowing()){
|
if(pausingPopUp.isShowing()){
|
||||||
pausingPopUp.hide();
|
pausingPopUp.hide();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Contribution.STATE_FAILED:
|
case Contribution.STATE_FAILED:
|
||||||
stateView.setVisibility(View.VISIBLE);
|
binding.contributionState.setVisibility(View.VISIBLE);
|
||||||
stateView.setText(R.string.contribution_state_failed);
|
binding.contributionState.setText(R.string.contribution_state_failed);
|
||||||
progressView.setVisibility(View.GONE);
|
binding.contributionProgress.setVisibility(View.GONE);
|
||||||
cancelButton.setVisibility(View.VISIBLE);
|
binding.cancelButton.setVisibility(View.VISIBLE);
|
||||||
retryButton.setVisibility(View.VISIBLE);
|
binding.retryButton.setVisibility(View.VISIBLE);
|
||||||
pauseResumeButton.setVisibility(View.GONE);
|
binding.pauseResumeButton.setVisibility(View.GONE);
|
||||||
imageOptions.setVisibility(View.VISIBLE);
|
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,11 +194,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
*/
|
*/
|
||||||
private void displayWikipediaButton(Boolean mediaExists) {
|
private void displayWikipediaButton(Boolean mediaExists) {
|
||||||
if (!mediaExists) {
|
if (!mediaExists) {
|
||||||
addToWikipediaButton.setVisibility(View.VISIBLE);
|
binding.wikipediaButton.setVisibility(View.VISIBLE);
|
||||||
isWikipediaButtonDisplayed = true;
|
isWikipediaButtonDisplayed = true;
|
||||||
cancelButton.setVisibility(View.GONE);
|
binding.cancelButton.setVisibility(View.GONE);
|
||||||
retryButton.setVisibility(View.GONE);
|
binding.retryButton.setVisibility(View.GONE);
|
||||||
imageOptions.setVisibility(View.VISIBLE);
|
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +220,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
/**
|
/**
|
||||||
* Retry upload when it is failed
|
* Retry upload when it is failed
|
||||||
*/
|
*/
|
||||||
@OnClick(R.id.retryButton)
|
|
||||||
public void retryUpload() {
|
public void retryUpload() {
|
||||||
callback.retryUpload(contribution);
|
callback.retryUpload(contribution);
|
||||||
}
|
}
|
||||||
|
|
@ -237,17 +227,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
/**
|
/**
|
||||||
* Delete a failed upload attempt
|
* Delete a failed upload attempt
|
||||||
*/
|
*/
|
||||||
@OnClick(R.id.cancelButton)
|
|
||||||
public void deleteUpload() {
|
public void deleteUpload() {
|
||||||
callback.deleteUpload(contribution);
|
callback.deleteUpload(contribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.contributionImage)
|
|
||||||
public void imageClicked() {
|
public void imageClicked() {
|
||||||
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
|
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.wikipediaButton)
|
|
||||||
public void wikipediaButtonClicked() {
|
public void wikipediaButtonClicked() {
|
||||||
callback.addImageToWikipedia(contribution);
|
callback.addImageToWikipedia(contribution);
|
||||||
}
|
}
|
||||||
|
|
@ -255,9 +242,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
/**
|
/**
|
||||||
* Triggers a callback for pause/resume
|
* Triggers a callback for pause/resume
|
||||||
*/
|
*/
|
||||||
@OnClick(R.id.pauseResumeButton)
|
|
||||||
public void onPauseResumeButtonClicked() {
|
public void onPauseResumeButtonClicked() {
|
||||||
if (pauseResumeButton.getTag().toString().equals("pause")) {
|
if (binding.pauseResumeButton.getTag().toString().equals("pause")) {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
resume();
|
resume();
|
||||||
|
|
@ -279,16 +265,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
* Update pause/resume button to show pause state
|
* Update pause/resume button to show pause state
|
||||||
*/
|
*/
|
||||||
private void setPaused() {
|
private void setPaused() {
|
||||||
pauseResumeButton.setImageResource(R.drawable.pause_icon);
|
binding.pauseResumeButton.setImageResource(R.drawable.pause_icon);
|
||||||
pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
|
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update pause/resume button to show resume state
|
* Update pause/resume button to show resume state
|
||||||
*/
|
*/
|
||||||
private void setResume() {
|
private void setResume() {
|
||||||
pauseResumeButton.setImageResource(R.drawable.play_icon);
|
binding.pauseResumeButton.setImageResource(R.drawable.play_icon);
|
||||||
pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
|
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageRequest getImageRequest() {
|
public ImageRequest getImageRequest() {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import static android.content.Context.SENSOR_SERVICE;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
||||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
||||||
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
||||||
|
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
|
@ -21,6 +28,9 @@ import android.widget.CheckBox;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
@ -29,17 +39,17 @@ import androidx.fragment.app.FragmentTransaction;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||||
import fr.free.nrw.commons.notification.models.Notification;
|
import fr.free.nrw.commons.notification.models.Notification;
|
||||||
import fr.free.nrw.commons.notification.NotificationController;
|
import fr.free.nrw.commons.notification.NotificationController;
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import androidx.work.WorkManager;
|
import androidx.work.WorkManager;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||||
|
|
@ -78,13 +88,22 @@ public class ContributionsFragment
|
||||||
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,7 +428,7 @@ 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,
|
||||||
|
|
@ -390,7 +442,7 @@ public class ContributionsFragment
|
||||||
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,31 +499,27 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,15 +534,18 @@ public class ContributionsFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
false)) // thanks to boolean, it will only return closest result
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::updateNearbyNotification,
|
.subscribe(this::updateNearbyNotification,
|
||||||
|
|
@ -492,26 +555,41 @@ public class ContributionsFragment
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
Place closestNearbyPlace = null;
|
||||||
|
// Find the first nearby place that has no image and exists
|
||||||
|
for (Place place : nearbyPlacesInfo.placeList) {
|
||||||
|
if (place.pic.equals("") && place.exists) {
|
||||||
|
closestNearbyPlace = place;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestNearbyPlace == null) {
|
||||||
|
binding.cardViewNearby.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
String distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location);
|
||||||
closestNearbyPlace.setDistance(distance);
|
closestNearbyPlace.setDistance(distance);
|
||||||
nearbyNotificationCardView.updateContent(closestNearbyPlace);
|
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
|
|
||||||
*/
|
*/
|
||||||
if (firstLocationUpdate) {
|
try {
|
||||||
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;
|
||||||
|
|
@ -724,9 +836,9 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
@ -738,7 +850,7 @@ public class ContributionsFragment
|
||||||
|
|
||||||
@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 {
|
||||||
|
|
@ -747,6 +859,17 @@ public class ContributionsFragment
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
float rotateDegree = Math.round(event.values[0]);
|
||||||
|
binding.cardViewNearby.rotateCompass(rotateDegree, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract for Contributions list View & Presenter
|
* The contract for Contributions list View & Presenter
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
|
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
|
||||||
|
|
||||||
|
import android.Manifest.permission;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -16,11 +17,12 @@ import android.view.ViewGroup;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ProgressBar;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
import android.widget.TextView;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
@ -28,26 +30,25 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
|
||||||
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
|
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
|
||||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||||
import butterknife.BindView;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
|
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,63 +61,72 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
|
|
||||||
private static final String RV_STATE = "rv_scroll_state";
|
private static final String RV_STATE = "rv_scroll_state";
|
||||||
|
|
||||||
@BindView(R.id.contributionsList)
|
|
||||||
RecyclerView rvContributionsList;
|
|
||||||
@BindView(R.id.loadingContributionsProgressBar)
|
|
||||||
ProgressBar progressBar;
|
|
||||||
@BindView(R.id.fab_plus)
|
|
||||||
FloatingActionButton fabPlus;
|
|
||||||
@BindView(R.id.fab_camera)
|
|
||||||
FloatingActionButton fabCamera;
|
|
||||||
@BindView(R.id.fab_gallery)
|
|
||||||
FloatingActionButton fabGallery;
|
|
||||||
@BindView(R.id.noContributionsYet)
|
|
||||||
TextView noContributionsYet;
|
|
||||||
@BindView(R.id.fab_layout)
|
|
||||||
LinearLayout fab_layout;
|
|
||||||
@BindView(R.id.fab_custom_gallery)
|
|
||||||
FloatingActionButton fabCustomGallery;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SystemThemeUtils systemThemeUtils;
|
SystemThemeUtils systemThemeUtils;
|
||||||
@BindView(R.id.tv_contributions_of_user)
|
|
||||||
AppCompatTextView tvContributionsOfUser;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ContributionController controller;
|
ContributionController controller;
|
||||||
@Inject
|
@Inject
|
||||||
MediaClient mediaClient;
|
MediaClient mediaClient;
|
||||||
|
|
||||||
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
||||||
@Inject
|
@Inject
|
||||||
WikiSite languageWikipediaSite;
|
WikiSite languageWikipediaSite;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ContributionsListPresenter contributionsListPresenter;
|
ContributionsListPresenter contributionsListPresenter;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
|
|
||||||
|
private FragmentContributionsListBinding binding;
|
||||||
private Animation fab_close;
|
private Animation fab_close;
|
||||||
private Animation fab_open;
|
private Animation fab_open;
|
||||||
private Animation rotate_forward;
|
private Animation rotate_forward;
|
||||||
private Animation rotate_backward;
|
private Animation rotate_backward;
|
||||||
|
|
||||||
|
|
||||||
private boolean isFabOpen;
|
private boolean isFabOpen;
|
||||||
|
|
||||||
private ContributionsListAdapter adapter;
|
@VisibleForTesting
|
||||||
|
protected RecyclerView rvContributionsList;
|
||||||
|
|
||||||
@Nullable private Callback callback;
|
@VisibleForTesting
|
||||||
|
protected ContributionsListAdapter adapter;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@VisibleForTesting
|
||||||
|
protected Callback callback;
|
||||||
|
|
||||||
private final int SPAN_COUNT_LANDSCAPE = 3;
|
private final int SPAN_COUNT_LANDSCAPE = 3;
|
||||||
private final int SPAN_COUNT_PORTRAIT = 1;
|
private final int SPAN_COUNT_PORTRAIT = 1;
|
||||||
|
|
||||||
private int contributionsSize;
|
private int contributionsSize;
|
||||||
String userName;
|
private String userName;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
|
new ActivityResultCallback<Map<String, Boolean>>() {
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(Map<String, Boolean> result) {
|
||||||
|
boolean areAllGranted = true;
|
||||||
|
for (final boolean b : result.values()) {
|
||||||
|
areAllGranted = areAllGranted && b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllGranted) {
|
||||||
|
controller.locationPermissionCallback.onLocationPermissionGranted();
|
||||||
|
} else {
|
||||||
|
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||||
|
controller.handleShowRationaleFlowCameraLocation(getActivity(),
|
||||||
|
inAppCameraLocationPermissionLauncher);
|
||||||
|
} else {
|
||||||
|
controller.locationPermissionCallback.onLocationPermissionDenied(
|
||||||
|
getActivity().getString(
|
||||||
|
R.string.in_app_camera_location_permission_denied));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) {
|
public void onCreate(
|
||||||
|
@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
//Now that we are allowing this fragment to be started for
|
//Now that we are allowing this fragment to be started for
|
||||||
// any userName- we expect it to be passed as an argument
|
// any userName- we expect it to be passed as an argument
|
||||||
|
|
@ -133,21 +143,36 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
public View onCreateView(
|
public View onCreateView(
|
||||||
final LayoutInflater inflater, @Nullable final ViewGroup container,
|
final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
binding = FragmentContributionsListBinding.inflate(
|
||||||
ButterKnife.bind(this, view);
|
inflater, container, false
|
||||||
|
);
|
||||||
|
rvContributionsList = binding.contributionsList;
|
||||||
|
|
||||||
contributionsListPresenter.onAttachView(this);
|
contributionsListPresenter.onAttachView(this);
|
||||||
|
binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector());
|
||||||
|
binding.fabCustomGallery.setOnLongClickListener(view -> {
|
||||||
|
ViewUtil.showShortToast(getContext(),R.string.custom_selector_title);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
if (Objects.equals(sessionManager.getUserName(), userName)) {
|
if (Objects.equals(sessionManager.getUserName(), userName)) {
|
||||||
tvContributionsOfUser.setVisibility(GONE);
|
binding.tvContributionsOfUser.setVisibility(GONE);
|
||||||
fab_layout.setVisibility(VISIBLE);
|
binding.fabLayout.setVisibility(VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
tvContributionsOfUser.setVisibility(VISIBLE);
|
binding.tvContributionsOfUser.setVisibility(VISIBLE);
|
||||||
tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
|
binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
|
||||||
fab_layout.setVisibility(GONE);
|
binding.fabLayout.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
initAdapter();
|
initAdapter();
|
||||||
return view;
|
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
binding = null;
|
||||||
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -280,7 +305,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
public void onConfigurationChanged(final Configuration newConfig) {
|
public void onConfigurationChanged(final Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
// check orientation
|
// check orientation
|
||||||
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
||||||
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
||||||
rvContributionsList
|
rvContributionsList
|
||||||
.setLayoutManager(
|
.setLayoutManager(
|
||||||
|
|
@ -295,22 +320,29 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setListeners() {
|
private void setListeners() {
|
||||||
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||||
fabCamera.setOnClickListener(view -> {
|
binding.fabCamera.setOnClickListener(view -> {
|
||||||
controller.initiateCameraPick(getActivity());
|
controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||||
animateFAB(isFabOpen);
|
animateFAB(isFabOpen);
|
||||||
});
|
});
|
||||||
fabGallery.setOnClickListener(view -> {
|
binding.fabCamera.setOnLongClickListener(view -> {
|
||||||
|
ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
binding.fabGallery.setOnClickListener(view -> {
|
||||||
controller.initiateGalleryPick(getActivity(), true);
|
controller.initiateGalleryPick(getActivity(), true);
|
||||||
animateFAB(isFabOpen);
|
animateFAB(isFabOpen);
|
||||||
});
|
});
|
||||||
|
binding.fabGallery.setOnLongClickListener(view -> {
|
||||||
|
ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch Custom Selector.
|
* Launch Custom Selector.
|
||||||
*/
|
*/
|
||||||
@OnClick(R.id.fab_custom_gallery)
|
protected void launchCustomSelector() {
|
||||||
void launchCustomSelector(){
|
|
||||||
controller.initiateCustomGalleryPickWithPermission(getActivity());
|
controller.initiateCustomGalleryPickWithPermission(getActivity());
|
||||||
animateFAB(isFabOpen);
|
animateFAB(isFabOpen);
|
||||||
}
|
}
|
||||||
|
|
@ -321,23 +353,23 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
|
|
||||||
private void animateFAB(final boolean isFabOpen) {
|
private void animateFAB(final boolean isFabOpen) {
|
||||||
this.isFabOpen = !isFabOpen;
|
this.isFabOpen = !isFabOpen;
|
||||||
if (fabPlus.isShown()) {
|
if (binding.fabPlus.isShown()) {
|
||||||
if (isFabOpen) {
|
if (isFabOpen) {
|
||||||
fabPlus.startAnimation(rotate_backward);
|
binding.fabPlus.startAnimation(rotate_backward);
|
||||||
fabCamera.startAnimation(fab_close);
|
binding.fabCamera.startAnimation(fab_close);
|
||||||
fabGallery.startAnimation(fab_close);
|
binding.fabGallery.startAnimation(fab_close);
|
||||||
fabCustomGallery.startAnimation(fab_close);
|
binding.fabCustomGallery.startAnimation(fab_close);
|
||||||
fabCamera.hide();
|
binding.fabCamera.hide();
|
||||||
fabGallery.hide();
|
binding.fabGallery.hide();
|
||||||
fabCustomGallery.hide();
|
binding.fabCustomGallery.hide();
|
||||||
} else {
|
} else {
|
||||||
fabPlus.startAnimation(rotate_forward);
|
binding.fabPlus.startAnimation(rotate_forward);
|
||||||
fabCamera.startAnimation(fab_open);
|
binding.fabCamera.startAnimation(fab_open);
|
||||||
fabGallery.startAnimation(fab_open);
|
binding.fabGallery.startAnimation(fab_open);
|
||||||
fabCustomGallery.startAnimation(fab_open);
|
binding.fabCustomGallery.startAnimation(fab_open);
|
||||||
fabCamera.show();
|
binding.fabCamera.show();
|
||||||
fabGallery.show();
|
binding.fabGallery.show();
|
||||||
fabCustomGallery.show();
|
binding.fabCustomGallery.show();
|
||||||
}
|
}
|
||||||
this.isFabOpen = !isFabOpen;
|
this.isFabOpen = !isFabOpen;
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +380,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void showWelcomeTip(final boolean shouldShow) {
|
public void showWelcomeTip(final boolean shouldShow) {
|
||||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -358,12 +390,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void showProgress(final boolean shouldShow) {
|
public void showProgress(final boolean shouldShow) {
|
||||||
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showNoContributionsUI(final boolean shouldShow) {
|
public void showNoContributionsUI(final boolean shouldShow) {
|
||||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -393,14 +425,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
@Override
|
@Override
|
||||||
public void deleteUpload(final Contribution contribution) {
|
public void deleteUpload(final Contribution contribution) {
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
DialogUtil.showAlertDialog(getActivity(),
|
||||||
String.format(getString(R.string.cancelling_upload),
|
String.format(Locale.getDefault(),
|
||||||
Locale.getDefault().getDisplayLanguage()),
|
getString(R.string.cancelling_upload)),
|
||||||
String.format(getString(R.string.cancel_upload_dialog),
|
String.format(Locale.getDefault(),
|
||||||
Locale.getDefault().getDisplayLanguage()),
|
getString(R.string.cancel_upload_dialog)),
|
||||||
"YES", "NO",
|
String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)),
|
||||||
() -> {
|
() -> {
|
||||||
ViewUtil.showShortToast(getContext(), R.string.cancelling_upload);
|
ViewUtil.showShortToast(getContext(), R.string.cancelling_upload);
|
||||||
contributionsListPresenter.deleteUpload(contribution);
|
contributionsListPresenter.deleteUpload(contribution);
|
||||||
|
CommonsApplication.cancelledUploads.add(contribution.getPageId());
|
||||||
}, () -> {
|
}, () -> {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
});
|
});
|
||||||
|
|
@ -422,8 +455,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
public void addImageToWikipedia(Contribution contribution) {
|
public void addImageToWikipedia(Contribution contribution) {
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
DialogUtil.showAlertDialog(getActivity(),
|
||||||
getString(R.string.add_picture_to_wikipedia_article_title),
|
getString(R.string.add_picture_to_wikipedia_article_title),
|
||||||
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
|
getString(R.string.add_picture_to_wikipedia_article_desc),
|
||||||
Locale.getDefault().getDisplayLanguage()),
|
|
||||||
() -> {
|
() -> {
|
||||||
showAddImageToWikipediaInstructions(contribution);
|
showAddImageToWikipediaInstructions(contribution);
|
||||||
}, () -> {
|
}, () -> {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import fr.free.nrw.commons.MediaDataExtractor;
|
import fr.free.nrw.commons.MediaDataExtractor;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||||
import io.reactivex.Scheduler;
|
import io.reactivex.Scheduler;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.functions.Action;
|
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
|
@ -76,11 +70,7 @@ public class ContributionsPresenter implements UserActionListener {
|
||||||
compositeDisposable.add(repository
|
compositeDisposable.add(repository
|
||||||
.save(contribution)
|
.save(contribution)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe(() -> {
|
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
||||||
WorkManager.getInstance(view.getContext().getApplicationContext())
|
view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP)));
|
||||||
.enqueueUniqueWork(
|
|
||||||
UploadWorker.class.getSimpleName(),
|
|
||||||
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,24 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import androidx.work.OneTimeWorkRequest;
|
import fr.free.nrw.commons.databinding.MainBinding;
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
|
|
@ -45,9 +41,13 @@ import fr.free.nrw.commons.notification.NotificationController;
|
||||||
import fr.free.nrw.commons.quiz.QuizChecker;
|
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -59,14 +59,8 @@ public class MainActivity extends BaseActivity
|
||||||
SessionManager sessionManager;
|
SessionManager sessionManager;
|
||||||
@Inject
|
@Inject
|
||||||
ContributionController controller;
|
ContributionController controller;
|
||||||
@BindView(R.id.toolbar)
|
@Inject
|
||||||
Toolbar toolbar;
|
ContributionDao contributionDao;
|
||||||
@BindView(R.id.pager)
|
|
||||||
public UnswipableViewPager viewPager;
|
|
||||||
@BindView(R.id.fragmentContainer)
|
|
||||||
public FrameLayout fragmentContainer;
|
|
||||||
@BindView(R.id.fragment_main_nav_tab_layout)
|
|
||||||
NavTabLayout tabLayout;
|
|
||||||
|
|
||||||
private ContributionsFragment contributionsFragment;
|
private ContributionsFragment contributionsFragment;
|
||||||
private NearbyParentFragment nearbyParentFragment;
|
private NearbyParentFragment nearbyParentFragment;
|
||||||
|
|
@ -91,6 +85,11 @@ public class MainActivity extends BaseActivity
|
||||||
|
|
||||||
public Menu menu;
|
public Menu menu;
|
||||||
|
|
||||||
|
public MainBinding binding;
|
||||||
|
|
||||||
|
NavTabLayout tabLayout;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumers should be simply using this method to use this activity.
|
* Consumers should be simply using this method to use this activity.
|
||||||
*
|
*
|
||||||
|
|
@ -118,11 +117,13 @@ public class MainActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = MainBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||||
|
tabLayout = binding.fragmentMainNavTabLayout;
|
||||||
loadLocale();
|
loadLocale();
|
||||||
setContentView(R.layout.main);
|
|
||||||
ButterKnife.bind(this);
|
binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> {
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
toolbar.setNavigationOnClickListener(view -> {
|
|
||||||
onSupportNavigateUp();
|
onSupportNavigateUp();
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
|
|
@ -139,6 +140,10 @@ public class MainActivity extends BaseActivity
|
||||||
setTitle(getString(R.string.navigation_item_explore));
|
setTitle(getString(R.string.navigation_item_explore));
|
||||||
setUpLoggedOutPager();
|
setUpLoggedOutPager();
|
||||||
} else {
|
} else {
|
||||||
|
if (applicationKvStore.getBoolean("firstrun", true)) {
|
||||||
|
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
|
||||||
|
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
|
||||||
|
}
|
||||||
if(savedInstanceState == null){
|
if(savedInstanceState == null){
|
||||||
//starting a fresh fragment.
|
//starting a fresh fragment.
|
||||||
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
|
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
|
||||||
|
|
@ -152,15 +157,29 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setUpPager();
|
setUpPager();
|
||||||
|
/**
|
||||||
|
* Ask the user for media location access just after login
|
||||||
|
* so that location in the EXIF metadata of the images shared by the user
|
||||||
|
* is retained on devices running Android 10 or above
|
||||||
|
*/
|
||||||
|
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||||
|
PermissionUtils.checkPermissionsAndPerformAction(
|
||||||
|
this,
|
||||||
|
() -> {},
|
||||||
|
R.string.media_location_permission_denied,
|
||||||
|
R.string.add_location_manually,
|
||||||
|
permission.ACCESS_MEDIA_LOCATION);
|
||||||
|
}
|
||||||
|
checkAndResumeStuckUploads();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedItemId(int id) {
|
public void setSelectedItemId(int id) {
|
||||||
tabLayout.setSelectedItemId(id);
|
binding.fragmentMainNavTabLayout.setSelectedItemId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setUpPager() {
|
private void setUpPager() {
|
||||||
tabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
|
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
|
||||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
if (!item.getTitle().equals(getString(R.string.more))) {
|
||||||
// do not change title for more fragment
|
// do not change title for more fragment
|
||||||
setTitle(item.getTitle());
|
setTitle(item.getTitle());
|
||||||
|
|
@ -175,7 +194,7 @@ public class MainActivity extends BaseActivity
|
||||||
|
|
||||||
private void setUpLoggedOutPager() {
|
private void setUpLoggedOutPager() {
|
||||||
loadFragment(ExploreFragment.newInstance(),false);
|
loadFragment(ExploreFragment.newInstance(),false);
|
||||||
tabLayout.setOnNavigationItemSelectedListener(item -> {
|
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> {
|
||||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
if (!item.getTitle().equals(getString(R.string.more))) {
|
||||||
// do not change title for more fragment
|
// do not change title for more fragment
|
||||||
setTitle(item.getTitle());
|
setTitle(item.getTitle());
|
||||||
|
|
@ -237,11 +256,11 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideTabs() {
|
public void hideTabs() {
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.fragmentMainNavTabLayout.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showTabs() {
|
public void showTabs() {
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.fragmentMainNavTabLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -259,6 +278,34 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the uploads that got stuck because of the app being killed
|
||||||
|
* or the device being rebooted.
|
||||||
|
*
|
||||||
|
* When the app is terminated or the device is restarted, contributions remain in the
|
||||||
|
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events.
|
||||||
|
* So, retrieving contributions labeled as 'STATE_IN_PROGRESS'
|
||||||
|
* from the database will provide the list of uploads that appear as stuck on opening the app again
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private void checkAndResumeStuckUploads() {
|
||||||
|
List<Contribution> stuckUploads = contributionDao.getContribution(
|
||||||
|
Collections.singletonList(Contribution.STATE_IN_PROGRESS))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.blockingGet();
|
||||||
|
Timber.d("Resuming " + stuckUploads.size() + " uploads...");
|
||||||
|
if(!stuckUploads.isEmpty()) {
|
||||||
|
for(Contribution contribution: stuckUploads) {
|
||||||
|
contribution.setState(Contribution.STATE_QUEUED);
|
||||||
|
Completable.fromAction(() -> contributionDao.saveSynchronous(contribution))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
||||||
|
this, ExistingWorkPolicy.APPEND_OR_REPLACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
@ -268,7 +315,7 @@ public class MainActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
|
outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem());
|
||||||
outState.putString("activeFragment", activeFragment.name());
|
outState.putString("activeFragment", activeFragment.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,6 +394,21 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry all failed uploads as soon as the user returns to the app
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private void retryAllFailedUploads() {
|
||||||
|
contributionDao.
|
||||||
|
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe(failedUploads -> {
|
||||||
|
for (Contribution contribution: failedUploads) {
|
||||||
|
contributionsFragment.retryUpload(contribution);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void toggleLimitedConnectionMode() {
|
public void toggleLimitedConnectionMode() {
|
||||||
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
||||||
!defaultKvStore
|
!defaultKvStore
|
||||||
|
|
@ -356,10 +418,8 @@ public class MainActivity extends BaseActivity
|
||||||
viewUtilWrapper
|
viewUtilWrapper
|
||||||
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
|
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
|
||||||
} else {
|
} else {
|
||||||
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
|
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
|
||||||
UploadWorker.class.getSimpleName(),
|
ExistingWorkPolicy.APPEND_OR_REPLACE);
|
||||||
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
|
|
||||||
|
|
||||||
viewUtilWrapper
|
viewUtilWrapper
|
||||||
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
|
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
|
||||||
}
|
}
|
||||||
|
|
@ -368,8 +428,6 @@ public class MainActivity extends BaseActivity
|
||||||
public void centerMapToPlace(Place place) {
|
public void centerMapToPlace(Place place) {
|
||||||
setSelectedItemId(NavTab.NEARBY.code());
|
setSelectedItemId(NavTab.NEARBY.code());
|
||||||
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() {
|
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() {
|
||||||
// if mapBox initialize in nearbyParentFragment then MapReady() function called
|
|
||||||
// so that nearbyParentFragemt.centerMaptoPlace(place) not throw any null pointer exception
|
|
||||||
@Override
|
@Override
|
||||||
public void onReady() {
|
public void onReady() {
|
||||||
nearbyParentFragment.centerMapToPlace(place);
|
nearbyParentFragment.centerMapToPlace(place);
|
||||||
|
|
@ -390,8 +448,11 @@ public class MainActivity extends BaseActivity
|
||||||
|
|
||||||
if ((applicationKvStore.getBoolean("firstrun", true)) &&
|
if ((applicationKvStore.getBoolean("firstrun", true)) &&
|
||||||
(!applicationKvStore.getBoolean("login_skipped"))) {
|
(!applicationKvStore.getBoolean("login_skipped"))) {
|
||||||
|
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
|
||||||
WelcomeActivity.startYourself(this);
|
WelcomeActivity.startYourself(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retryAllFailedUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -407,7 +468,7 @@ public class MainActivity extends BaseActivity
|
||||||
* Public method to show nearby from the reference of this.
|
* Public method to show nearby from the reference of this.
|
||||||
*/
|
*/
|
||||||
public void showNearby() {
|
public void showNearby() {
|
||||||
tabLayout.setSelectedItemId(NavTab.NEARBY.code());
|
binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code());
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ActiveFragment {
|
public enum ActiveFragment {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.WallpaperManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.Worker;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
import com.facebook.common.executors.CallerThreadExecutor;
|
||||||
|
import com.facebook.common.references.CloseableReference;
|
||||||
|
import com.facebook.datasource.DataSource;
|
||||||
|
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||||
|
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||||
|
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||||
|
import com.facebook.imagepipeline.image.CloseableImage;
|
||||||
|
import com.facebook.imagepipeline.request.ImageRequest;
|
||||||
|
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import java.io.IOException;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class SetWallpaperWorker extends Worker {
|
||||||
|
|
||||||
|
private static final String NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel";
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
public SetWallpaperWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||||
|
super(context, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Result doWork() {
|
||||||
|
Context context = getApplicationContext();
|
||||||
|
createNotificationChannel(context);
|
||||||
|
showProgressNotification(context);
|
||||||
|
|
||||||
|
String imageUrl = getInputData().getString("imageUrl");
|
||||||
|
if (imageUrl == null) {
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageRequest imageRequest = ImageRequestBuilder
|
||||||
|
.newBuilderWithSource(Uri.parse(imageUrl))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||||
|
final DataSource<CloseableReference<CloseableImage>>
|
||||||
|
dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
|
||||||
|
|
||||||
|
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
||||||
|
@Override
|
||||||
|
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||||
|
if (dataSource.isFinished() && bitmap != null) {
|
||||||
|
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
||||||
|
setWallpaper(context, Bitmap.createBitmap(bitmap));
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailureImpl(DataSource dataSource) {
|
||||||
|
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
|
||||||
|
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.");
|
||||||
|
if (dataSource != null) {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, CallerThreadExecutor.getInstance());
|
||||||
|
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWallpaper(Context context, Bitmap bitmap) {
|
||||||
|
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
|
||||||
|
|
||||||
|
try {
|
||||||
|
wallpaperManager.setBitmap(bitmap);
|
||||||
|
showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully.");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Timber.e(e, "Error setting wallpaper");
|
||||||
|
showNotification(context, "Setting Wallpaper Failed", " "+e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProgressNotification(Context context) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.commons_logo)
|
||||||
|
.setContentTitle("Setting Wallpaper")
|
||||||
|
.setContentText("Please wait...")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(0, 0, true);
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNotification(Context context, String title, String content) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.commons_logo)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setOngoing(false);
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel(Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
CharSequence name = "Wallpaper Setting";
|
||||||
|
String description = "Notifications for wallpaper setting progress";
|
||||||
|
int importance = NotificationManager.IMPORTANCE_HIGH;
|
||||||
|
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
|
||||||
|
channel.setDescription(description);
|
||||||
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
|
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
|
||||||
private val SWIPE_VELOCITY_THRESHOLD = 1000
|
private val SWIPE_VELOCITY_THRESHOLD = 1000
|
||||||
|
|
||||||
override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
|
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean {
|
||||||
return gestureDetector.onTouchEvent(motionEvent)
|
return gestureDetector.onTouchEvent(motionEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
|
|
||||||
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
|
||||||
override fun onDown(e: MotionEvent?): Boolean {
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
|
||||||
|
|
@ -85,6 +86,8 @@ class ImageAdapter(
|
||||||
*/
|
*/
|
||||||
private var actionableImagesMap: TreeMap<Int, Image> = TreeMap()
|
private var actionableImagesMap: TreeMap<Int, Image> = TreeMap()
|
||||||
|
|
||||||
|
private var uploadingContributionList: List<Contribution> = ArrayList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores already added positions of actionable images
|
* Stores already added positions of actionable images
|
||||||
*/
|
*/
|
||||||
|
|
@ -119,6 +122,7 @@ class ImageAdapter(
|
||||||
* Bind View holder, load image, selected view, click listeners.
|
* Bind View holder, load image, selected view, click listeners.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||||
|
|
||||||
var image=images[position]
|
var image=images[position]
|
||||||
holder.image.setImageDrawable (null)
|
holder.image.setImageDrawable (null)
|
||||||
if (context.contentResolver.getType(image.uri) == null) {
|
if (context.contentResolver.getType(image.uri) == null) {
|
||||||
|
|
@ -151,13 +155,12 @@ class ImageAdapter(
|
||||||
|
|
||||||
val isSelected = selectedIndex != -1
|
val isSelected = selectedIndex != -1
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
holder.itemSelected(selectedImages.size)
|
holder.itemSelected()
|
||||||
} else {
|
} else {
|
||||||
holder.itemUnselected()
|
holder.itemUnselected()
|
||||||
}
|
}
|
||||||
|
|
||||||
imageLoader.queryAndSetView(
|
imageLoader.queryAndSetView(
|
||||||
holder, image, ioDispatcher, defaultDispatcher
|
holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList
|
||||||
)
|
)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val sharedPreferences: SharedPreferences =
|
val sharedPreferences: SharedPreferences =
|
||||||
|
|
@ -168,16 +171,18 @@ 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)
|
||||||
|
if(actionableImages.size > position) {
|
||||||
image = actionableImages[position]
|
image = actionableImages[position]
|
||||||
Glide.with(holder.image).load(image.uri)
|
Glide.with(holder.image).load(image.uri)
|
||||||
.thumbnail(0.3f).into(holder.image)
|
.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
|
||||||
// further operations
|
// further operations
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
imageFragment?.dismissMarkUnmarkProgressDialog()
|
||||||
val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout)
|
val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout)
|
||||||
bottomLayout.visibility = View.GONE
|
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) {
|
||||||
|
if (title.isNotEmpty()){
|
||||||
val titleText = findViewById<TextView>(R.id.title)
|
val titleText = findViewById<TextView>(R.id.title)
|
||||||
|
var titleWithAppendedImageCount = title
|
||||||
|
if (selectedImageCount > 0) {
|
||||||
|
titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix,
|
||||||
|
selectedImageCount, selectedImageCount)})"
|
||||||
|
}
|
||||||
if (titleText != null) {
|
if (titleText != null) {
|
||||||
titleText.text = title
|
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)
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -99,6 +103,10 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
*/
|
*/
|
||||||
private var progressLayout: ConstraintLayout? = null
|
private var progressLayout: ConstraintLayout? = null
|
||||||
|
|
||||||
|
private lateinit var progressDialog: AlertDialog
|
||||||
|
private lateinit var progressDialogLayout: ProgressDialogBinding
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NotForUploadStatus Dao class for database operations
|
* NotForUploadStatus Dao class for database operations
|
||||||
*/
|
*/
|
||||||
|
|
@ -129,6 +137,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mediaClient: MediaClient
|
lateinit var mediaClient: MediaClient
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var contributionDao: ContributionDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -163,7 +174,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
bucketId = arguments?.getLong(BUCKET_ID)
|
bucketId = arguments?.getLong(BUCKET_ID)
|
||||||
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
||||||
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java)
|
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
||||||
|
CustomSelectorViewModel::class.java
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -171,17 +184,22 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
* Init imageAdapter, gridLayoutManger.
|
* Init imageAdapter, gridLayoutManger.
|
||||||
* SetUp recycler view.
|
* SetUp recycler view.
|
||||||
*/
|
*/
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||||
imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
imageAdapter =
|
||||||
gridLayoutManager = GridLayoutManager(context,getSpanCount())
|
ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
||||||
with(binding?.selectorRv){
|
gridLayoutManager = GridLayoutManager(context, getSpanCount())
|
||||||
|
with(binding?.selectorRv) {
|
||||||
this?.layoutManager = gridLayoutManager
|
this?.layoutManager = gridLayoutManager
|
||||||
this?.setHasFixedSize(true)
|
this?.setHasFixedSize(true)
|
||||||
this?.adapter = imageAdapter
|
this?.adapter = imageAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel?.result?.observe(viewLifecycleOwner, Observer{
|
viewModel?.result?.observe(viewLifecycleOwner, Observer {
|
||||||
handleResult(it)
|
handleResult(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -194,9 +212,16 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
|
|
||||||
val sharedPreferences: SharedPreferences =
|
val sharedPreferences: SharedPreferences =
|
||||||
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
|
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
|
||||||
showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
showAlreadyActionedImages =
|
||||||
|
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||||
switch?.isChecked = showAlreadyActionedImages
|
switch?.isChecked = showAlreadyActionedImages
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(requireActivity())
|
||||||
|
builder.setCancelable(false)
|
||||||
|
progressDialogLayout = ProgressDialogBinding.inflate(layoutInflater, container, false)
|
||||||
|
builder.setView(progressDialogLayout.root)
|
||||||
|
progressDialog = builder.create()
|
||||||
|
|
||||||
return binding?.root
|
return binding?.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +242,8 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
editor.apply()
|
editor.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
imageAdapter.init(allImages, allImages, TreeMap())
|
val uploadingContributions = getUploadingContributions()
|
||||||
|
imageAdapter.init(allImages, allImages, TreeMap(), uploadingContributions)
|
||||||
imageAdapter.notifyDataSetChanged()
|
imageAdapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,13 +262,15 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
/**
|
/**
|
||||||
* Handle view model result.
|
* Handle view model result.
|
||||||
*/
|
*/
|
||||||
private fun handleResult(result:Result){
|
private fun handleResult(result: Result) {
|
||||||
if(result.status is CallbackStatus.SUCCESS){
|
if (result.status is CallbackStatus.SUCCESS) {
|
||||||
val images = result.images
|
val images = result.images
|
||||||
if(images.isNotEmpty()) {
|
|
||||||
|
val uploadingContributions = getUploadingContributions()
|
||||||
|
if (images.isNotEmpty()) {
|
||||||
filteredImages = ImageHelper.filterImages(images, bucketId)
|
filteredImages = ImageHelper.filterImages(images, bucketId)
|
||||||
allImages = ArrayList(filteredImages)
|
allImages = ArrayList(filteredImages)
|
||||||
imageAdapter.init(filteredImages, allImages, TreeMap())
|
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||||
selectorRV?.let {
|
selectorRV?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
lastItemId?.let { pos ->
|
lastItemId?.let { pos ->
|
||||||
|
|
@ -250,18 +278,18 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
|
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
binding?.emptyText?.let {
|
binding?.emptyText?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
selectorRV?.let{
|
selectorRV?.let {
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loader?.let {
|
loader?.let {
|
||||||
it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
|
it.visibility =
|
||||||
|
if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,19 +345,61 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
imageAdapter.refresh(filteredImages, allImages)
|
imageAdapter.refresh(filteredImages, allImages, getUploadingContributions())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the image from the actionable image map
|
||||||
|
*/
|
||||||
|
fun removeImage(image : Image){
|
||||||
|
imageAdapter.removeImageFromActionableImageMap(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the selected images
|
||||||
|
*/
|
||||||
|
fun clearSelectedImages() {
|
||||||
|
imageAdapter.clearSelectedImages()
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Passes selected images and other information from Activity to Fragment and connects it with
|
* Passes selected images and other information from Activity to Fragment and connects it with
|
||||||
* the adapter
|
* the adapter
|
||||||
*/
|
*/
|
||||||
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean){
|
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) {
|
||||||
imageAdapter.setSelectedImages(selectedImages)
|
imageAdapter.setSelectedImages(selectedImages)
|
||||||
|
|
||||||
|
val uploadingContributions = getUploadingContributions()
|
||||||
|
|
||||||
if (!showAlreadyActionedImages && shouldRefresh) {
|
if (!showAlreadyActionedImages && shouldRefresh) {
|
||||||
imageAdapter.init(filteredImages, allImages, TreeMap())
|
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||||
imageAdapter.setSelectedImages(selectedImages)
|
imageAdapter.setSelectedImages(selectedImages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows mark/unmark progress dialog
|
||||||
|
*/
|
||||||
|
fun showMarkUnmarkProgressDialog(text: String) {
|
||||||
|
if (!progressDialog.isShowing) {
|
||||||
|
progressDialogLayout.progressDialogText.text = text
|
||||||
|
progressDialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses mark/unmark progress dialog
|
||||||
|
*/
|
||||||
|
fun dismissMarkUnmarkProgressDialog() {
|
||||||
|
if (progressDialog.isShowing) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUploadingContributions(): List<Contribution> {
|
||||||
|
|
||||||
|
return contributionDao.getContribution(
|
||||||
|
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED)
|
||||||
|
)?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
|
|
@ -75,7 +76,8 @@ class ImageLoader @Inject constructor(
|
||||||
holder: ImageViewHolder,
|
holder: ImageViewHolder,
|
||||||
image: Image,
|
image: Image,
|
||||||
ioDispatcher: CoroutineDispatcher,
|
ioDispatcher: CoroutineDispatcher,
|
||||||
defaultDispatcher: CoroutineDispatcher
|
defaultDispatcher: CoroutineDispatcher,
|
||||||
|
uploadedContributionsList : List<Contribution>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,6 +86,7 @@ class ImageLoader @Inject constructor(
|
||||||
mapHolderImage[holder] = image
|
mapHolderImage[holder] = image
|
||||||
holder.itemNotUploaded()
|
holder.itemNotUploaded()
|
||||||
holder.itemForUpload()
|
holder.itemForUpload()
|
||||||
|
holder.itemNotUploading()
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
var result: Result = Result.NOTFOUND
|
var result: Result = Result.NOTFOUND
|
||||||
|
|
@ -214,6 +217,17 @@ class ImageLoader @Inject constructor(
|
||||||
holder.itemNotForUpload()
|
holder.itemNotForUpload()
|
||||||
} else holder.itemForUpload()
|
} else holder.itemForUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uploadedContributionsList.isNotEmpty()) {
|
||||||
|
for (contribution in uploadedContributionsList ) {
|
||||||
|
if (contribution.contentUri == image.uri && showAlreadyActionedImages) {
|
||||||
|
holder.itemUploading()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
holder.itemNotUploading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,17 +237,22 @@ class ImageLoader @Inject constructor(
|
||||||
suspend fun nextActionableImage(
|
suspend fun nextActionableImage(
|
||||||
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
||||||
defaultDispatcher: CoroutineDispatcher,
|
defaultDispatcher: CoroutineDispatcher,
|
||||||
nextImagePosition: Int
|
nextImagePosition: Int,
|
||||||
|
currentlyUploadingImages: List<Contribution>
|
||||||
): Int {
|
): Int {
|
||||||
var next: Int
|
var next: Int
|
||||||
|
|
||||||
// Traversing from given position to the end
|
// Traversing from given position to the end
|
||||||
for (i in nextImagePosition until allImages.size){
|
for (i in nextImagePosition until allImages.size){
|
||||||
val it = allImages[i]
|
val currentImage = allImages[i]
|
||||||
val imageSHA1: String = when (mapImageSHA1[it.uri] != null) {
|
|
||||||
true -> mapImageSHA1[it.uri]!!
|
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
||||||
|
continue // Skip this image as it's currently being uploaded
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) {
|
||||||
|
true -> mapImageSHA1[currentImage.uri]!!
|
||||||
else -> CustomSelectorUtils.getImageSHA1(
|
else -> CustomSelectorUtils.getImageSHA1(
|
||||||
it.uri,
|
currentImage.uri,
|
||||||
ioDispatcher,
|
ioDispatcher,
|
||||||
fileUtilsWrapper,
|
fileUtilsWrapper,
|
||||||
context.contentResolver
|
context.contentResolver
|
||||||
|
|
@ -253,7 +272,7 @@ class ImageLoader @Inject constructor(
|
||||||
// If the image is not present in the already uploaded table, checks for its
|
// If the image is not present in the already uploaded table, checks for its
|
||||||
// modified SHA1 in already uploaded table
|
// modified SHA1 in already uploaded table
|
||||||
if (next <= 0) {
|
if (next <= 0) {
|
||||||
val modifiedImageSha1 = getSHA1(it, defaultDispatcher)
|
val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher)
|
||||||
next = uploadedStatusDao.findByModifiedImageSHA1(
|
next = uploadedStatusDao.findByModifiedImageSHA1(
|
||||||
modifiedImageSha1,
|
modifiedImageSha1,
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import androidx.room.TypeConverters
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.*
|
import fr.free.nrw.commons.customselector.database.*
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.PlaceDao
|
||||||
import fr.free.nrw.commons.review.ReviewDao
|
import fr.free.nrw.commons.review.ReviewDao
|
||||||
import fr.free.nrw.commons.review.ReviewEntity
|
import fr.free.nrw.commons.review.ReviewEntity
|
||||||
import fr.free.nrw.commons.upload.depicts.Depicts
|
import fr.free.nrw.commons.upload.depicts.Depicts
|
||||||
|
|
@ -15,10 +17,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||||
* The database for accessing the respective DAOs
|
* The database for accessing the respective DAOs
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 15, exportSchema = false)
|
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], version = 18, exportSchema = false)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun contributionDao(): ContributionDao
|
abstract fun contributionDao(): ContributionDao
|
||||||
|
abstract fun PlaceDao(): PlaceDao
|
||||||
abstract fun DepictsDao(): DepictsDao;
|
abstract fun DepictsDao(): DepictsDao;
|
||||||
abstract fun UploadedStatusDao(): UploadedStatusDao;
|
abstract fun UploadedStatusDao(): UploadedStatusDao;
|
||||||
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.contributions.ChunkInfo;
|
import fr.free.nrw.commons.contributions.ChunkInfo;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
|
import fr.free.nrw.commons.nearby.Sitelinks;
|
||||||
import fr.free.nrw.commons.upload.WikidataPlace;
|
import fr.free.nrw.commons.upload.WikidataPlace;
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -134,6 +136,18 @@ public class Converters {
|
||||||
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
|
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static Sitelinks sitelinksFromString(String value) {
|
||||||
|
Type type = new TypeToken<Sitelinks>() {}.getType();
|
||||||
|
return new Gson().fromJson(value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String fromSitelinks(Sitelinks sitelinks) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
return gson.toJson(sitelinks);
|
||||||
|
}
|
||||||
|
|
||||||
private static String writeObjectToString(Object object) {
|
private static String writeObjectToString(Object object) {
|
||||||
return object == null ? null : getGson().toJson(object);
|
return object == null ? null : getGson().toJson(object);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.actions.PageEditClient;
|
import fr.free.nrw.commons.actions.PageEditClient;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||||
|
|
@ -66,7 +67,13 @@ public class DeleteHelper {
|
||||||
|
|
||||||
return delete(media, reason)
|
return delete(media, reason)
|
||||||
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
||||||
.firstOrError();
|
.firstOrError()
|
||||||
|
.onErrorResumeNext(throwable -> {
|
||||||
|
if (throwable instanceof InvalidLoginTokenException) {
|
||||||
|
return Single.error(throwable);
|
||||||
|
}
|
||||||
|
return Single.error(throwable);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -104,21 +111,29 @@ public class DeleteHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||||
|
.onErrorResumeNext(throwable -> {
|
||||||
|
if (throwable instanceof InvalidLoginTokenException) {
|
||||||
|
return Observable.error(throwable);
|
||||||
|
}
|
||||||
|
return Observable.error(throwable);
|
||||||
|
})
|
||||||
.flatMap(result -> {
|
.flatMap(result -> {
|
||||||
if (result) {
|
if (result) {
|
||||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Failed to nominate for deletion");
|
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||||
}).flatMap(result -> {
|
})
|
||||||
|
.flatMap(result -> {
|
||||||
if (result) {
|
if (result) {
|
||||||
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Failed to nominate for deletion");
|
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||||
}).flatMap(result -> {
|
})
|
||||||
|
.flatMap(result -> {
|
||||||
if (result) {
|
if (result) {
|
||||||
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
|
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Failed to nominate for deletion");
|
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,6 +220,8 @@ public class DeleteHelper {
|
||||||
});
|
});
|
||||||
|
|
||||||
alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> {
|
alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> {
|
||||||
|
reviewCallback.disableButtons();
|
||||||
|
|
||||||
|
|
||||||
String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " ";
|
String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " ";
|
||||||
|
|
||||||
|
|
@ -224,13 +241,15 @@ public class DeleteHelper {
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(aBoolean -> {
|
.subscribe(aBoolean -> {
|
||||||
if (aBoolean) {
|
|
||||||
reviewCallback.onSuccess();
|
reviewCallback.onSuccess();
|
||||||
|
}, throwable -> {
|
||||||
|
if (throwable instanceof InvalidLoginTokenException) {
|
||||||
|
reviewCallback.onTokenException((InvalidLoginTokenException) throwable);
|
||||||
} else {
|
} else {
|
||||||
reviewCallback.onFailure();
|
reviewCallback.onFailure();
|
||||||
}
|
}
|
||||||
|
reviewCallback.enableButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure());
|
alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure());
|
||||||
d = alert.create();
|
d = alert.create();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package fr.free.nrw.commons.delete;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.wikipedia.util.DateUtil;
|
import fr.free.nrw.commons.utils.DateUtil;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
package fr.free.nrw.commons.description
|
package fr.free.nrw.commons.description
|
||||||
|
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.speech.RecognizerIntent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding
|
import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding
|
||||||
import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION
|
import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION
|
||||||
import fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT
|
|
||||||
import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT
|
import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT
|
||||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||||
import fr.free.nrw.commons.settings.Prefs
|
import fr.free.nrw.commons.settings.Prefs
|
||||||
|
|
@ -18,8 +23,13 @@ import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetail
|
import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.functions.Consumer
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for populating and editing existing description and caption
|
* Activity for populating and editing existing description and caption
|
||||||
*/
|
*/
|
||||||
|
|
@ -40,6 +50,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
*/
|
*/
|
||||||
var wikiText: String? = null
|
var wikiText: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Media object
|
||||||
|
*/
|
||||||
|
var media: Media? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saved language
|
* Saved language
|
||||||
*/
|
*/
|
||||||
|
|
@ -55,6 +70,15 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
|
|
||||||
private lateinit var binding: ActivityDescriptionEditBinding
|
private lateinit var binding: ActivityDescriptionEditBinding
|
||||||
|
|
||||||
|
private val REQUEST_CODE_FOR_VOICE_INPUT = 1213
|
||||||
|
|
||||||
|
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||||
|
|
||||||
|
@Inject lateinit var descriptionEditHelper: DescriptionEditHelper
|
||||||
|
|
||||||
|
@Inject lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -62,13 +86,21 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val bundle = intent.extras
|
val bundle = intent.extras
|
||||||
val descriptionAndCaptions: ArrayList<UploadMediaDetail> =
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
descriptionAndCaptions = savedInstanceState.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)
|
||||||
|
wikiText = savedInstanceState.getString(WIKITEXT)
|
||||||
|
savedLanguageValue = savedInstanceState.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
||||||
|
media = savedInstanceState.getParcelable("media")
|
||||||
|
} else {
|
||||||
|
descriptionAndCaptions =
|
||||||
bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!!
|
bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!!
|
||||||
wikiText = bundle.getString(WIKITEXT)
|
wikiText = bundle.getString(WIKITEXT)
|
||||||
savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
||||||
|
media = bundle.getParcelable("media")
|
||||||
|
}
|
||||||
initRecyclerView(descriptionAndCaptions)
|
initRecyclerView(descriptionAndCaptions)
|
||||||
|
|
||||||
binding.btnAddDescription.setOnClickListener(::onButtonAddDescriptionClicked)
|
|
||||||
binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked)
|
binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked)
|
||||||
binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked)
|
binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked)
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +110,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
* @param descriptionAndCaptions list of description and caption
|
* @param descriptionAndCaptions list of description and caption
|
||||||
*/
|
*/
|
||||||
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
||||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(
|
uploadMediaDetailAdapter = UploadMediaDetailAdapter(this,
|
||||||
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
|
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
|
||||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||||
showInfoAlert(
|
showInfoAlert(
|
||||||
|
|
@ -107,17 +139,20 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
|
|
||||||
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {}
|
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {}
|
||||||
|
|
||||||
private fun onBackButtonClicked(view: View) {
|
/**
|
||||||
onBackPressed()
|
* Adds new language item to RecyclerView
|
||||||
}
|
*/
|
||||||
|
override fun addLanguage() {
|
||||||
private fun onButtonAddDescriptionClicked(view: View) {
|
|
||||||
val uploadMediaDetail = UploadMediaDetail()
|
val uploadMediaDetail = UploadMediaDetail()
|
||||||
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
|
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
|
||||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
||||||
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onBackButtonClicked(view: View) {
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onSubmitButtonClicked(view: View) {
|
private fun onSubmitButtonClicked(view: View) {
|
||||||
showLoggingProgressBar()
|
showLoggingProgressBar()
|
||||||
val uploadMediaDetails = uploadMediaDetailAdapter.items
|
val uploadMediaDetails = uploadMediaDetailAdapter.items
|
||||||
|
|
@ -151,22 +186,85 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
buffer.append(uploadDetails.languageCode)
|
buffer.append(uploadDetails.languageCode)
|
||||||
buffer.append("|1=")
|
buffer.append("|1=")
|
||||||
buffer.append(uploadDetails.descriptionText)
|
buffer.append(uploadDetails.descriptionText)
|
||||||
buffer.append("}}, ")
|
buffer.append("}}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.replace(", $".toRegex(), "")
|
buffer.replace(", $".toRegex(), "")
|
||||||
buffer.append(descriptionEnd)
|
buffer.append(descriptionEnd)
|
||||||
}
|
}
|
||||||
val returningIntent = Intent()
|
editDescription(media!!, buffer.toString(), uploadMediaDetails as ArrayList<UploadMediaDetail>)
|
||||||
returningIntent.putExtra(UPDATED_WIKITEXT, buffer.toString())
|
|
||||||
returningIntent.putParcelableArrayListExtra(
|
|
||||||
LIST_OF_DESCRIPTION_AND_CAPTION,
|
|
||||||
uploadMediaDetails as ArrayList<out Parcelable?>
|
|
||||||
)
|
|
||||||
setResult(RESULT_OK, returningIntent)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits description and caption
|
||||||
|
* @param media media object
|
||||||
|
* @param updatedWikiText updated wiki text
|
||||||
|
* @param uploadMediaDetails descriptions and captions
|
||||||
|
*/
|
||||||
|
private fun editDescription(media : Media, updatedWikiText : String, uploadMediaDetails : ArrayList<UploadMediaDetail>){
|
||||||
|
|
||||||
|
try {
|
||||||
|
descriptionEditHelper?.addDescription(
|
||||||
|
applicationContext, media,
|
||||||
|
updatedWikiText
|
||||||
|
)
|
||||||
|
?.subscribeOn(Schedulers.io())
|
||||||
|
?.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })?.let {
|
||||||
|
compositeDisposable.add(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e : InvalidLoginTokenException) {
|
||||||
|
val username: String? = sessionManager?.userName
|
||||||
|
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||||
|
this,
|
||||||
|
getString(R.string.invalid_login_message),
|
||||||
|
username
|
||||||
|
)
|
||||||
|
|
||||||
|
val commonsApplication = CommonsApplication.getInstance()
|
||||||
|
if (commonsApplication != null ){
|
||||||
|
commonsApplication.clearApplicationData(this,logoutListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val updatedCaptions = LinkedHashMap<String, String>()
|
||||||
|
for (mediaDetail in uploadMediaDetails) {
|
||||||
|
try {
|
||||||
|
compositeDisposable.add(
|
||||||
|
descriptionEditHelper!!.addCaption(
|
||||||
|
applicationContext, media,
|
||||||
|
mediaDetail.languageCode, mediaDetail.captionText
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { s: Boolean? ->
|
||||||
|
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
|
||||||
|
media.captions = updatedCaptions
|
||||||
|
Timber.d("Caption is added.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (e : InvalidLoginTokenException) {
|
||||||
|
val username = sessionManager.userName
|
||||||
|
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||||
|
this,
|
||||||
|
getString(R.string.invalid_login_message),
|
||||||
|
username
|
||||||
|
)
|
||||||
|
|
||||||
|
val commonsApplication = CommonsApplication.getInstance()
|
||||||
|
if (commonsApplication != null ){
|
||||||
|
commonsApplication.clearApplicationData(this,logoutListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showLoggingProgressBar() {
|
private fun showLoggingProgressBar() {
|
||||||
progressDialog = ProgressDialog(this)
|
progressDialog = ProgressDialog(this)
|
||||||
progressDialog!!.isIndeterminate = true
|
progressDialog!!.isIndeterminate = true
|
||||||
|
|
@ -175,4 +273,24 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
progressDialog!!.setCanceledOnTouchOutside(false)
|
progressDialog!!.setCanceledOnTouchOutside(false)
|
||||||
progressDialog!!.show()
|
progressDialog!!.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override
|
||||||
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
|
||||||
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
|
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS )
|
||||||
|
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) }
|
||||||
|
else { Timber.e("Error %s", resultCode) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
|
||||||
|
outState.putString(WIKITEXT, wikiText)
|
||||||
|
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
|
||||||
|
//save Media
|
||||||
|
outState.putParcelable("media", media)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import fr.free.nrw.commons.description.DescriptionEditActivity;
|
||||||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
||||||
import fr.free.nrw.commons.explore.SearchActivity;
|
import fr.free.nrw.commons.explore.SearchActivity;
|
||||||
import fr.free.nrw.commons.media.ZoomableActivity;
|
import fr.free.nrw.commons.media.ZoomableActivity;
|
||||||
|
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||||
import fr.free.nrw.commons.review.ReviewActivity;
|
import fr.free.nrw.commons.review.ReviewActivity;
|
||||||
|
|
@ -79,4 +80,7 @@ public abstract class ActivityBuilderModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract ZoomableActivity bindZoomableActivity();
|
abstract ZoomableActivity bindZoomableActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract WikidataFeedback bindWikiFeedback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.actions.PageEditClient;
|
||||||
import fr.free.nrw.commons.explore.categories.CategoriesModule;
|
import fr.free.nrw.commons.explore.categories.CategoriesModule;
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||||
import fr.free.nrw.commons.navtab.NavTabLayout;
|
import fr.free.nrw.commons.nearby.NearbyController;
|
||||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
|
@ -69,6 +70,9 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
||||||
|
|
||||||
void inject(PicOfDayAppWidget picOfDayAppWidget);
|
void inject(PicOfDayAppWidget picOfDayAppWidget);
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
void inject(NearbyController nearbyController);
|
||||||
|
|
||||||
Gson gson();
|
Gson gson();
|
||||||
|
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.db.AppDatabase;
|
import fr.free.nrw.commons.db.AppDatabase;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
|
import fr.free.nrw.commons.nearby.PlaceDao;
|
||||||
import fr.free.nrw.commons.review.ReviewDao;
|
import fr.free.nrw.commons.review.ReviewDao;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.upload.UploadController;
|
import fr.free.nrw.commons.upload.UploadController;
|
||||||
|
|
@ -41,7 +42,6 @@ import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Dependency Provider class for Commons Android.
|
* The Dependency Provider class for Commons Android.
|
||||||
|
|
@ -257,8 +257,8 @@ public class CommonsApplicationModule {
|
||||||
|
|
||||||
@Named("username")
|
@Named("username")
|
||||||
@Provides
|
@Provides
|
||||||
public String provideLoggedInUsername() {
|
public String provideLoggedInUsername(SessionManager sessionManager) {
|
||||||
return Objects.toString(AppAdapter.get().getUserName(), "");
|
return Objects.toString(sessionManager.getUserName(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -276,6 +276,11 @@ public class CommonsApplicationModule {
|
||||||
return appDatabase.contributionDao();
|
return appDatabase.contributionDao();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public PlaceDao providesPlaceDao(AppDatabase appDatabase) {
|
||||||
|
return appDatabase.PlaceDao();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the reference of DepictsDao class.
|
* Get the reference of DepictsDao class.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,16 @@ import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BetaConstants;
|
import fr.free.nrw.commons.BetaConstants;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
import fr.free.nrw.commons.OkHttpConnectionFactory;
|
||||||
import fr.free.nrw.commons.actions.PageEditClient;
|
import fr.free.nrw.commons.actions.PageEditClient;
|
||||||
import fr.free.nrw.commons.actions.PageEditInterface;
|
import fr.free.nrw.commons.actions.PageEditInterface;
|
||||||
|
import fr.free.nrw.commons.actions.ThanksInterface;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface;
|
||||||
|
import fr.free.nrw.commons.auth.csrf.LogoutClient;
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient;
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginInterface;
|
||||||
import fr.free.nrw.commons.category.CategoryInterface;
|
import fr.free.nrw.commons.category.CategoryInterface;
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
|
@ -18,11 +26,15 @@ import fr.free.nrw.commons.media.PageMediaInterface;
|
||||||
import fr.free.nrw.commons.media.WikidataMediaInterface;
|
import fr.free.nrw.commons.media.WikidataMediaInterface;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import fr.free.nrw.commons.mwapi.UserInterface;
|
import fr.free.nrw.commons.mwapi.UserInterface;
|
||||||
|
import fr.free.nrw.commons.notification.NotificationInterface;
|
||||||
import fr.free.nrw.commons.review.ReviewInterface;
|
import fr.free.nrw.commons.review.ReviewInterface;
|
||||||
import fr.free.nrw.commons.upload.UploadInterface;
|
import fr.free.nrw.commons.upload.UploadInterface;
|
||||||
import fr.free.nrw.commons.upload.WikiBaseInterface;
|
import fr.free.nrw.commons.upload.WikiBaseInterface;
|
||||||
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
||||||
|
import fr.free.nrw.commons.wikidata.CommonsServiceFactory;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataInterface;
|
import fr.free.nrw.commons.wikidata.WikidataInterface;
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -33,31 +45,25 @@ import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
import okhttp3.logging.HttpLoggingInterceptor;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
||||||
import org.wikipedia.csrf.CsrfTokenClient;
|
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||||
import org.wikipedia.dataclient.Service;
|
import fr.free.nrw.commons.wikidata.GsonUtil;
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.json.GsonUtil;
|
|
||||||
import org.wikipedia.login.LoginClient;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public class NetworkingModule {
|
public class NetworkingModule {
|
||||||
private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql";
|
private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql";
|
||||||
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/urbanecmbot/commonsmisc";
|
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app";
|
||||||
|
|
||||||
private static final String TEST_TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app";
|
|
||||||
|
|
||||||
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
|
|
||||||
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
|
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
|
||||||
private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite";
|
private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite";
|
||||||
|
|
||||||
public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite";
|
public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite";
|
||||||
|
|
||||||
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
|
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
|
||||||
|
public static final String NAMED_WIKI_CSRF = "wiki-csrf";
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
@ -73,6 +79,12 @@ public class NetworkingModule {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) {
|
||||||
|
return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar));
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public HttpLoggingInterceptor provideHttpLoggingInterceptor() {
|
public HttpLoggingInterceptor provideHttpLoggingInterceptor() {
|
||||||
|
|
@ -88,29 +100,86 @@ public class NetworkingModule {
|
||||||
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
|
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
|
||||||
DepictsClient depictsClient,
|
DepictsClient depictsClient,
|
||||||
@Named("tools_forge") HttpUrl toolsForgeUrl,
|
@Named("tools_forge") HttpUrl toolsForgeUrl,
|
||||||
@Named("test_tools_forge") HttpUrl testToolsForgeUrl,
|
|
||||||
@Named("default_preferences") JsonKvStore defaultKvStore,
|
@Named("default_preferences") JsonKvStore defaultKvStore,
|
||||||
Gson gson) {
|
Gson gson) {
|
||||||
return new OkHttpJsonApiClient(okHttpClient,
|
return new OkHttpJsonApiClient(okHttpClient,
|
||||||
depictsClient,
|
depictsClient,
|
||||||
toolsForgeUrl,
|
toolsForgeUrl,
|
||||||
testToolsForgeUrl,
|
|
||||||
WIKIDATA_SPARQL_QUERY_URL,
|
WIKIDATA_SPARQL_QUERY_URL,
|
||||||
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
||||||
gson);
|
gson);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named(NAMED_COMMONS_CSRF)
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public CommonsCookieStorage provideCookieStorage(
|
||||||
return new CsrfTokenClient(commonsWikiSite, commonsWikiSite);
|
@Named("default_preferences") JsonKvStore preferences) {
|
||||||
|
CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences);
|
||||||
|
cookieStorage.load();
|
||||||
|
return cookieStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public LoginClient provideLoginClient() {
|
public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) {
|
||||||
return new LoginClient();
|
return new CommonsCookieJar(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Named(NAMED_COMMONS_CSRF)
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager,
|
||||||
|
@Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
|
||||||
|
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a singleton instance of CsrfTokenClient for Wikidata.
|
||||||
|
*
|
||||||
|
* @param sessionManager The session manager to manage user sessions.
|
||||||
|
* @param tokenInterface The interface for obtaining CSRF tokens.
|
||||||
|
* @param loginClient The client for handling login operations.
|
||||||
|
* @param logoutClient The client for handling logout operations.
|
||||||
|
* @return A singleton instance of CsrfTokenClient.
|
||||||
|
*/
|
||||||
|
@Named(NAMED_WIKI_CSRF)
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager,
|
||||||
|
@Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
|
||||||
|
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a singleton instance of CsrfTokenInterface for Wikidata.
|
||||||
|
*
|
||||||
|
* @param serviceFactory The factory used to create service interfaces.
|
||||||
|
* @return A singleton instance of CsrfTokenInterface for Wikidata.
|
||||||
|
*/
|
||||||
|
@Named("wikidata-csrf-interface")
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Named("commons-csrf-interface")
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public LoginClient provideLoginClient(LoginInterface loginInterface) {
|
||||||
|
return new LoginClient(loginInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -129,21 +198,6 @@ public class NetworkingModule {
|
||||||
return HttpUrl.parse(TOOLS_FORGE_URL);
|
return HttpUrl.parse(TOOLS_FORGE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Named("test_tools_forge")
|
|
||||||
@NonNull
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
public HttpUrl provideTestToolsForgeUrl() {
|
|
||||||
return HttpUrl.parse(TEST_TOOLS_FORGE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
|
||||||
public WikiSite provideCommonsWikiSite() {
|
|
||||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named(NAMED_WIKI_DATA_WIKI_SITE)
|
@Named(NAMED_WIKI_DATA_WIKI_SITE)
|
||||||
|
|
@ -164,54 +218,40 @@ public class NetworkingModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named("commons-service")
|
public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) {
|
||||||
public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class);
|
||||||
return ServiceFactory.get(commonsWikiSite);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named("wikidata-service")
|
public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) {
|
||||||
public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class);
|
||||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class);
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
public WikiBaseInterface provideWikiBaseInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, WikiBaseInterface.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named("commons-page-edit-service")
|
@Named("commons-page-edit-service")
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named("wikidata-page-edit-service")
|
@Named("wikidata-page-edit-service")
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
|
public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class);
|
return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named("commons-page-edit")
|
@Named("commons-page-edit")
|
||||||
|
|
@ -222,10 +262,25 @@ public class NetworkingModule {
|
||||||
return new PageEditClient(csrfTokenClient, pageEditInterface);
|
return new PageEditClient(csrfTokenClient, pageEditInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a singleton instance of PageEditClient for Wikidata.
|
||||||
|
*
|
||||||
|
* @param csrfTokenClient The client used to manage CSRF tokens.
|
||||||
|
* @param pageEditInterface The interface for page edit operations.
|
||||||
|
* @return A singleton instance of PageEditClient for Wikidata.
|
||||||
|
*/
|
||||||
|
@Named("wikidata-page-edit")
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient,
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
|
@Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) {
|
||||||
|
return new PageEditClient(csrfTokenClient, pageEditInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -236,36 +291,44 @@ public class NetworkingModule {
|
||||||
*/
|
*/
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public WikidataMediaInterface provideWikidataMediaInterface(
|
public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) {
|
||||||
@Named(NAMED_COMMONS_WIKI_SITE) final WikiSite commonsWikiSite) {
|
return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class);
|
||||||
return ServiceFactory.get(commonsWikiSite,
|
|
||||||
BetaConstants.COMMONS_URL, WikidataMediaInterface.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) {
|
public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public CategoryInterface provideCategoryInterface(
|
public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) {
|
||||||
@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class);
|
||||||
return ServiceFactory
|
|
||||||
.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UserInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public WikidataInterface provideWikidataInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
|
public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, WikidataInterface.class);
|
return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) {
|
||||||
|
return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -274,8 +337,8 @@ public class NetworkingModule {
|
||||||
*/
|
*/
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite) {
|
public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) {
|
||||||
return ServiceFactory.get(wikiSite, wikiSite.url(), PageMediaInterface.class);
|
return serviceFactory.create(wikiSite.url(), PageMediaInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
301
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
301
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.Animator.AnimatorListener
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.media.ExifInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.graphics.rotationMatrix
|
||||||
|
import androidx.core.graphics.scaleMatrix
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityEditBinding
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity class for editing and rotating images using LLJTran with EXIF attribute preservation.
|
||||||
|
*
|
||||||
|
* This activity allows loads an image, allows users to rotate it by 90-degree increments, and
|
||||||
|
* save the edited image while preserving its EXIF attributes. The class includes methods
|
||||||
|
* for initializing the UI, animating image rotations, copying EXIF data, and handling
|
||||||
|
* the image-saving process.
|
||||||
|
*/
|
||||||
|
class EditActivity : AppCompatActivity() {
|
||||||
|
private var imageUri = ""
|
||||||
|
private lateinit var vm: EditViewModel
|
||||||
|
private val sourceExifAttributeList = mutableListOf<Pair<String, String?>>()
|
||||||
|
private lateinit var binding: ActivityEditBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityEditBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
supportActionBar?.title = ""
|
||||||
|
val intent = intent
|
||||||
|
imageUri = intent.getStringExtra("image") ?: ""
|
||||||
|
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
||||||
|
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
||||||
|
val exifTags = arrayOf(
|
||||||
|
ExifInterface.TAG_APERTURE,
|
||||||
|
ExifInterface.TAG_DATETIME,
|
||||||
|
ExifInterface.TAG_EXPOSURE_TIME,
|
||||||
|
ExifInterface.TAG_FLASH,
|
||||||
|
ExifInterface.TAG_FOCAL_LENGTH,
|
||||||
|
ExifInterface.TAG_GPS_ALTITUDE,
|
||||||
|
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_DATESTAMP,
|
||||||
|
ExifInterface.TAG_GPS_LATITUDE,
|
||||||
|
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_LONGITUDE,
|
||||||
|
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||||
|
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||||
|
ExifInterface.TAG_IMAGE_LENGTH,
|
||||||
|
ExifInterface.TAG_IMAGE_WIDTH,
|
||||||
|
ExifInterface.TAG_ISO,
|
||||||
|
ExifInterface.TAG_MAKE,
|
||||||
|
ExifInterface.TAG_MODEL,
|
||||||
|
ExifInterface.TAG_ORIENTATION,
|
||||||
|
ExifInterface.TAG_WHITE_BALANCE,
|
||||||
|
ExifInterface.WHITEBALANCE_AUTO,
|
||||||
|
ExifInterface.WHITEBALANCE_MANUAL
|
||||||
|
)
|
||||||
|
for (tag in exifTags) {
|
||||||
|
val attribute = sourceExif?.getAttribute(tag.toString())
|
||||||
|
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the ImageView and associated UI elements.
|
||||||
|
*
|
||||||
|
* This function sets up the ImageView for displaying an image, adjusts its view bounds,
|
||||||
|
* and scales the initial image to fit within the ImageView. It also sets click listeners
|
||||||
|
* for the "Rotate" and "Save" buttons.
|
||||||
|
*/
|
||||||
|
private fun init() {
|
||||||
|
binding.iv.adjustViewBounds = true
|
||||||
|
binding.iv.scaleType = ImageView.ScaleType.MATRIX
|
||||||
|
binding.iv.post(Runnable {
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeFile(imageUri, options)
|
||||||
|
|
||||||
|
val bitmapWidth = options.outWidth
|
||||||
|
val bitmapHeight = options.outHeight
|
||||||
|
|
||||||
|
// Check if the bitmap dimensions exceed a certain threshold
|
||||||
|
val maxBitmapSize = 2000 // Set your maximum size here
|
||||||
|
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
||||||
|
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
||||||
|
options.inSampleSize = scaleFactor
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||||
|
binding.iv.setImageBitmap(scaledBitmap)
|
||||||
|
// Update the ImageView with the scaled bitmap
|
||||||
|
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
||||||
|
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
||||||
|
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||||
|
binding.iv.setImageBitmap(bitmap)
|
||||||
|
|
||||||
|
val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat()
|
||||||
|
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
|
||||||
|
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
binding.rotateBtn.setOnClickListener {
|
||||||
|
animateImageHeight()
|
||||||
|
}
|
||||||
|
binding.btnSave.setOnClickListener {
|
||||||
|
getRotatedImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageRotation = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the height, rotation, and scale of an ImageView to provide a smooth
|
||||||
|
* transition effect when rotating an image by 90 degrees.
|
||||||
|
*
|
||||||
|
* This function calculates the new height, rotation, and scale for the ImageView
|
||||||
|
* based on the current image rotation angle and animates the changes using a
|
||||||
|
* ValueAnimator. It also disables a rotate button during the animation to prevent
|
||||||
|
* further rotation actions.
|
||||||
|
*/
|
||||||
|
private fun animateImageHeight() {
|
||||||
|
val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat()
|
||||||
|
val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat()
|
||||||
|
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
|
||||||
|
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
|
||||||
|
val rotation = imageRotation % 360
|
||||||
|
val newRotation = rotation + 90
|
||||||
|
|
||||||
|
val newViewHeight: Int
|
||||||
|
val imageScale: Float
|
||||||
|
val newImageScale: Float
|
||||||
|
|
||||||
|
Timber.d("Rotation $rotation")
|
||||||
|
Timber.d("new Rotation $newRotation")
|
||||||
|
|
||||||
|
|
||||||
|
if (rotation == 0 || rotation == 180) {
|
||||||
|
imageScale = viewWidth / drawableWidth
|
||||||
|
newImageScale = viewWidth / drawableHeight
|
||||||
|
newViewHeight = (drawableWidth * newImageScale).toInt()
|
||||||
|
} else if (rotation == 90 || rotation == 270) {
|
||||||
|
imageScale = viewWidth / drawableHeight
|
||||||
|
newImageScale = viewWidth / drawableWidth
|
||||||
|
newViewHeight = (drawableHeight * newImageScale).toInt()
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("rotation can 0, 90, 180 or 270. \${rotation} is unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L)
|
||||||
|
|
||||||
|
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
|
||||||
|
animator.addListener(object : AnimatorListener {
|
||||||
|
override fun onAnimationStart(animation: Animator) {
|
||||||
|
binding.rotateBtn.setEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
imageRotation = newRotation % 360
|
||||||
|
binding.rotateBtn.setEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
animator.addUpdateListener { animation ->
|
||||||
|
val animVal = animation.animatedValue as Float
|
||||||
|
val complementaryAnimVal = 1 - animVal
|
||||||
|
val animatedHeight =
|
||||||
|
(complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt()
|
||||||
|
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
||||||
|
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
||||||
|
binding.iv.getLayoutParams().height = animatedHeight
|
||||||
|
val matrix: Matrix = rotationMatrix(
|
||||||
|
animatedRotation,
|
||||||
|
drawableWidth / 2,
|
||||||
|
drawableHeight / 2
|
||||||
|
)
|
||||||
|
matrix.postScale(
|
||||||
|
animatedScale,
|
||||||
|
animatedScale,
|
||||||
|
drawableWidth / 2,
|
||||||
|
drawableHeight / 2
|
||||||
|
)
|
||||||
|
matrix.postTranslate(
|
||||||
|
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
|
||||||
|
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2
|
||||||
|
)
|
||||||
|
binding.iv.setImageMatrix(matrix)
|
||||||
|
binding.iv.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates and edits the current image, copies EXIF data, and returns the edited image path.
|
||||||
|
*
|
||||||
|
* This function retrieves the path of the current image specified by `imageUri`,
|
||||||
|
* rotates it based on the `imageRotation` angle using the `rotateImage` method
|
||||||
|
* from the `vm`, and updates the EXIF attributes of the
|
||||||
|
* rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data
|
||||||
|
* using the `copyExifData` method, creates an Intent to return the edited image's file path
|
||||||
|
* as a result, and finishes the current activity.
|
||||||
|
*/
|
||||||
|
fun getRotatedImage() {
|
||||||
|
|
||||||
|
val filePath = imageUri.toUri().path
|
||||||
|
val file = filePath?.let { File(it) }
|
||||||
|
|
||||||
|
|
||||||
|
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
||||||
|
if (rotatedImage == null) {
|
||||||
|
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
val editedImageExif: ExifInterface?
|
||||||
|
if (rotatedImage?.path != null) {
|
||||||
|
editedImageExif = ExifInterface(rotatedImage.path)
|
||||||
|
copyExifData(editedImageExif)
|
||||||
|
}
|
||||||
|
val resultIntent = Intent()
|
||||||
|
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
|
||||||
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object.
|
||||||
|
*
|
||||||
|
* This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes
|
||||||
|
* on the provided `editedImageExif` object.
|
||||||
|
*
|
||||||
|
* @param editedImageExif The ExifInterface object for the edited image.
|
||||||
|
*/
|
||||||
|
private fun copyExifData(editedImageExif: ExifInterface?) {
|
||||||
|
|
||||||
|
for (attr in sourceExifAttributeList) {
|
||||||
|
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||||
|
editedImageExif!!.setAttribute(attr.first, attr.second)
|
||||||
|
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||||
|
}
|
||||||
|
|
||||||
|
editedImageExif?.saveAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the scale factor to be used for scaling down a bitmap based on its original
|
||||||
|
* dimensions and the maximum allowed size.
|
||||||
|
* @param originalWidth The original width of the bitmap.
|
||||||
|
* @param originalHeight The original height of the bitmap.
|
||||||
|
* @param maxSize The maximum allowed size for either width or height.
|
||||||
|
* @return The scale factor to be used for scaling down the bitmap.
|
||||||
|
* If the bitmap is smaller than or equal to the maximum size in both dimensions,
|
||||||
|
* the scale factor is 1.
|
||||||
|
* If the bitmap is larger than the maximum size in either dimension,
|
||||||
|
* the scale factor is calculated as the largest power of 2 that is less than or equal
|
||||||
|
* to the ratio of the original dimension to the maximum size.
|
||||||
|
* The scale factor ensures that the scaled bitmap will fit within the maximum size
|
||||||
|
* while maintaining aspect ratio.
|
||||||
|
*/
|
||||||
|
private fun calculateScaleFactor(originalWidth: Int, originalHeight: Int, maxSize: Int): Int {
|
||||||
|
var scaleFactor = 1
|
||||||
|
|
||||||
|
if (originalWidth > maxSize || originalHeight > maxSize) {
|
||||||
|
// Calculate the largest power of 2 that is less than or equal to the desired width and height
|
||||||
|
val widthRatio = Math.ceil((originalWidth.toDouble() / maxSize.toDouble())).toInt()
|
||||||
|
val heightRatio = Math.ceil((originalHeight.toDouble() / maxSize.toDouble())).toInt()
|
||||||
|
|
||||||
|
scaleFactor = if (widthRatio > heightRatio) widthRatio else heightRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
return scaleFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for image editing operations.
|
||||||
|
*
|
||||||
|
* This ViewModel class is responsible for managing image editing operations, such as
|
||||||
|
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
||||||
|
*/
|
||||||
|
class EditViewModel() : ViewModel() {
|
||||||
|
|
||||||
|
// Ideally should be injected using DI
|
||||||
|
private val transformImage: TransformImage = TransformImageImpl()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
fun rotateImage(degree: Int, imageFile: File): File? {
|
||||||
|
return transformImage.rotateImage(imageFile, degree)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for image transformation operations.
|
||||||
|
*
|
||||||
|
* This interface defines a contract for image transformation operations, allowing
|
||||||
|
* implementations to provide specific functionality for tasks like rotating images.
|
||||||
|
*/
|
||||||
|
interface TransformImage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
fun rotateImage(imageFile: File, degree : Int ):File?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import android.mediautil.image.jpeg.LLJTran
|
||||||
|
import android.mediautil.image.jpeg.LLJTranException
|
||||||
|
import android.os.Environment
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the TransformImage interface for image rotation operations.
|
||||||
|
*
|
||||||
|
* This class provides an implementation for the TransformImage interface, right now it exposes a
|
||||||
|
* function for rotating images by a specified degree using the LLJTran library. Right now it reads
|
||||||
|
* the input image file, performs the rotation, and saves the rotated image to a new file.
|
||||||
|
*/
|
||||||
|
class TransformImageImpl() : TransformImage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
override fun rotateImage(imageFile: File, degree : Int): File? {
|
||||||
|
|
||||||
|
Timber.tag("Trying to rotate image").d("Starting")
|
||||||
|
|
||||||
|
val path = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)
|
||||||
|
|
||||||
|
val imagePath = System.currentTimeMillis()
|
||||||
|
val file: File = File(path, "$imagePath.jpg")
|
||||||
|
|
||||||
|
val output = file
|
||||||
|
|
||||||
|
val rotated = try {
|
||||||
|
val lljTran = LLJTran(imageFile)
|
||||||
|
lljTran.read(
|
||||||
|
LLJTran.READ_ALL,
|
||||||
|
false,
|
||||||
|
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||||
|
lljTran.transform(
|
||||||
|
when(degree){
|
||||||
|
90 -> LLJTran.ROT_90
|
||||||
|
180 -> LLJTran.ROT_180
|
||||||
|
270 -> LLJTran.ROT_270
|
||||||
|
else -> {
|
||||||
|
LLJTran.ROT_90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
|
||||||
|
)
|
||||||
|
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
||||||
|
lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
|
||||||
|
}
|
||||||
|
lljTran.freeMemory()
|
||||||
|
true
|
||||||
|
} catch (e: LLJTranException) {
|
||||||
|
Timber.tag("Error").d(e)
|
||||||
|
return null
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotated) {
|
||||||
|
Timber.tag("Done rotating image").d("Done")
|
||||||
|
Timber.tag("Add").d(output.absolutePath)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentExploreBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
import fr.free.nrw.commons.theme.BaseActivity;
|
||||||
|
|
@ -33,10 +32,8 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
private static final String EXPLORE_MAP = "Map";
|
private static final String EXPLORE_MAP = "Map";
|
||||||
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
|
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
|
||||||
|
|
||||||
@BindView(R.id.tab_layout)
|
|
||||||
TabLayout tabLayout;
|
public FragmentExploreBinding binding;
|
||||||
@BindView(R.id.viewPager)
|
|
||||||
ParentViewPager viewPager;
|
|
||||||
ViewPagerAdapter viewPagerAdapter;
|
ViewPagerAdapter viewPagerAdapter;
|
||||||
private ExploreListRootFragment featuredRootFragment;
|
private ExploreListRootFragment featuredRootFragment;
|
||||||
private ExploreListRootFragment mobileRootFragment;
|
private ExploreListRootFragment mobileRootFragment;
|
||||||
|
|
@ -46,7 +43,10 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
public JsonKvStore applicationKvStore;
|
public JsonKvStore applicationKvStore;
|
||||||
|
|
||||||
public void setScroll(boolean canScroll){
|
public void setScroll(boolean canScroll){
|
||||||
viewPager.setCanScroll(canScroll);
|
if (binding != null)
|
||||||
|
{
|
||||||
|
binding.viewPager.setCanScroll(canScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
@ -56,22 +56,17 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.fragment_explore, container, false);
|
binding = FragmentExploreBinding.inflate(inflater, container, false);
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
|
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
|
||||||
viewPager.setAdapter(viewPagerAdapter);
|
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||||
viewPager.setId(R.id.viewPager);
|
binding.viewPager.setId(R.id.viewPager);
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
viewPager.addOnPageChangeListener(new OnPageChangeListener() {
|
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrolled(int position, float positionOffset,
|
public void onPageScrolled(int position, float positionOffset,
|
||||||
int positionOffsetPixels) {
|
int positionOffsetPixels) {
|
||||||
|
|
@ -81,9 +76,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
@Override
|
@Override
|
||||||
public void onPageSelected(int position) {
|
public void onPageSelected(int position) {
|
||||||
if (position == 2) {
|
if (position == 2) {
|
||||||
viewPager.setCanScroll(false);
|
binding.viewPager.setCanScroll(false);
|
||||||
} else {
|
} else {
|
||||||
viewPager.setCanScroll(true);
|
binding.viewPager.setCanScroll(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +89,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
});
|
});
|
||||||
setTabs();
|
setTabs();
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
return view;
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,13 +128,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
if (tabLayout.getSelectedTabPosition() == 0) {
|
if (binding.tabLayout.getSelectedTabPosition() == 0) {
|
||||||
if (featuredRootFragment.backPressed()) {
|
if (featuredRootFragment.backPressed()) {
|
||||||
((BaseActivity) getActivity()).getSupportActionBar()
|
((BaseActivity) getActivity()).getSupportActionBar()
|
||||||
.setDisplayHomeAsUpEnabled(false);
|
.setDisplayHomeAsUpEnabled(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
|
} else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
|
||||||
if (mobileRootFragment.backPressed()) {
|
if (mobileRootFragment.backPressed()) {
|
||||||
((BaseActivity) getActivity()).getSupportActionBar()
|
((BaseActivity) getActivity()).getSupportActionBar()
|
||||||
.setDisplayHomeAsUpEnabled(false);
|
.setDisplayHomeAsUpEnabled(false);
|
||||||
|
|
@ -180,6 +175,12 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ import android.widget.FrameLayout;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
|
|
@ -26,8 +25,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
private CategoriesMediaFragment listFragment;
|
private CategoriesMediaFragment listFragment;
|
||||||
|
|
||||||
@BindView(R.id.explore_container)
|
private FragmentFeaturedRootBinding binding;
|
||||||
FrameLayout container;
|
|
||||||
|
|
||||||
public ExploreListRootFragment() {
|
public ExploreListRootFragment() {
|
||||||
//empty constructor necessary otherwise crashes on recreate
|
//empty constructor necessary otherwise crashes on recreate
|
||||||
|
|
@ -47,9 +45,9 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
|
||||||
ButterKnife.bind(this, view);
|
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||||
return view;
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -109,9 +107,13 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int position) {
|
public void onMediaClicked(int position) {
|
||||||
container.setVisibility(View.VISIBLE);
|
if (binding!=null) {
|
||||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
}
|
||||||
|
if (((ExploreFragment) getParentFragment()).binding!=null) {
|
||||||
|
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
((ExploreFragment) getParentFragment()).setScroll(false);
|
((ExploreFragment) getParentFragment()).setScroll(false);
|
||||||
setFragment(mediaDetails, listFragment);
|
setFragment(mediaDetails, listFragment);
|
||||||
mediaDetails.showImage(position);
|
mediaDetails.showImage(position);
|
||||||
|
|
@ -185,16 +187,29 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
||||||
*/
|
*/
|
||||||
public boolean backPressed() {
|
public boolean backPressed() {
|
||||||
if (null != mediaDetails && mediaDetails.isVisible()) {
|
if (null != mediaDetails && mediaDetails.isVisible()) {
|
||||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
|
if (((ExploreFragment) getParentFragment()).binding != null) {
|
||||||
|
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
removeFragment(mediaDetails);
|
removeFragment(mediaDetails);
|
||||||
((ExploreFragment) getParentFragment()).setScroll(true);
|
((ExploreFragment) getParentFragment()).setScroll(true);
|
||||||
setFragment(listFragment, mediaDetails);
|
setFragment(listFragment, mediaDetails);
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
if (((MainActivity) getActivity()) != null) {
|
||||||
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (((MainActivity) getActivity()) != null) {
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ import android.widget.FrameLayout;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
|
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||||
|
|
@ -26,8 +25,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
private ExploreMapFragment mapFragment;
|
private ExploreMapFragment mapFragment;
|
||||||
|
|
||||||
@BindView(R.id.explore_container)
|
private FragmentFeaturedRootBinding binding;
|
||||||
FrameLayout container;
|
|
||||||
|
|
||||||
public ExploreMapRootFragment() {
|
public ExploreMapRootFragment() {
|
||||||
//empty constructor necessary otherwise crashes on recreate
|
//empty constructor necessary otherwise crashes on recreate
|
||||||
|
|
@ -54,9 +52,10 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
|
||||||
ButterKnife.bind(this, view);
|
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||||
return view;
|
|
||||||
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -116,9 +115,9 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int position) {
|
public void onMediaClicked(int position) {
|
||||||
container.setVisibility(View.VISIBLE);
|
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
((ExploreFragment) getParentFragment()).setScroll(false);
|
((ExploreFragment) getParentFragment()).setScroll(false);
|
||||||
setFragment(mediaDetails, mapFragment);
|
setFragment(mediaDetails, mapFragment);
|
||||||
mediaDetails.showImage(position);
|
mediaDetails.showImage(position);
|
||||||
|
|
@ -192,7 +191,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
*/
|
*/
|
||||||
public boolean backPressed() {
|
public boolean backPressed() {
|
||||||
if (null != mediaDetails && mediaDetails.isVisible()) {
|
if (null != mediaDetails && mediaDetails.isVisible()) {
|
||||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
|
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
removeFragment(mediaDetails);
|
removeFragment(mediaDetails);
|
||||||
((ExploreFragment) getParentFragment()).setScroll(true);
|
((ExploreFragment) getParentFragment()).setScroll(true);
|
||||||
setFragment(mapFragment, mediaDetails);
|
setFragment(mapFragment, mediaDetails);
|
||||||
|
|
@ -213,4 +212,11 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
||||||
((MainActivity) getActivity()).showTabs();
|
((MainActivity) getActivity()).showTabs();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,17 @@ package fr.free.nrw.commons.explore;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
import com.jakewharton.rxbinding2.view.RxView;
|
||||||
import com.jakewharton.rxbinding2.widget.RxSearchView;
|
import com.jakewharton.rxbinding2.widget.RxSearchView;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||||
|
import fr.free.nrw.commons.databinding.ActivitySearchBinding;
|
||||||
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
|
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
|
||||||
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
|
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
|
||||||
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
|
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
|
||||||
|
|
@ -45,13 +39,6 @@ import timber.log.Timber;
|
||||||
public class SearchActivity extends BaseActivity
|
public class SearchActivity extends BaseActivity
|
||||||
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||||
|
|
||||||
@BindView(R.id.toolbar_search) Toolbar toolbar;
|
|
||||||
@BindView(R.id.searchHistoryContainer) FrameLayout searchHistoryContainer;
|
|
||||||
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
|
|
||||||
@BindView(R.id.searchBox) SearchView searchView;
|
|
||||||
@BindView(R.id.tab_layout) TabLayout tabLayout;
|
|
||||||
@BindView(R.id.viewPager) ViewPager viewPager;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
RecentSearchesDao recentSearchesDao;
|
RecentSearchesDao recentSearchesDao;
|
||||||
|
|
||||||
|
|
@ -63,25 +50,28 @@ public class SearchActivity extends BaseActivity
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
private MediaDetailPagerFragment mediaDetails;
|
||||||
ViewPagerAdapter viewPagerAdapter;
|
ViewPagerAdapter viewPagerAdapter;
|
||||||
|
|
||||||
|
private ActivitySearchBinding binding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_search);
|
binding = ActivitySearchBinding.inflate(getLayoutInflater());
|
||||||
ButterKnife.bind(this);
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
setTitle(getString(R.string.title_activity_search));
|
setTitle(getString(R.string.title_activity_search));
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(binding.toolbarSearch);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
toolbar.setNavigationOnClickListener(v->onBackPressed());
|
binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed());
|
||||||
supportFragmentManager = getSupportFragmentManager();
|
supportFragmentManager = getSupportFragmentManager();
|
||||||
setSearchHistoryFragment();
|
setSearchHistoryFragment();
|
||||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||||
viewPager.setAdapter(viewPagerAdapter);
|
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||||
viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
|
binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
setTabs();
|
setTabs();
|
||||||
searchView.setQueryHint(getString(R.string.search_commons));
|
binding.searchBox.setQueryHint(getString(R.string.search_commons));
|
||||||
searchView.onActionViewExpanded();
|
binding.searchBox.onActionViewExpanded();
|
||||||
searchView.clearFocus();
|
binding.searchBox.clearFocus();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,8 +103,8 @@ public class SearchActivity extends BaseActivity
|
||||||
|
|
||||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||||
viewPagerAdapter.notifyDataSetChanged();
|
viewPagerAdapter.notifyDataSetChanged();
|
||||||
compositeDisposable.add(RxSearchView.queryTextChanges(searchView)
|
compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox)
|
||||||
.takeUntil(RxView.detaches(searchView))
|
.takeUntil(RxView.detaches(binding.searchBox))
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::handleSearch, Timber::e
|
.subscribe(this::handleSearch, Timber::e
|
||||||
|
|
@ -124,9 +114,9 @@ public class SearchActivity extends BaseActivity
|
||||||
private void handleSearch(final CharSequence query) {
|
private void handleSearch(final CharSequence query) {
|
||||||
if (!TextUtils.isEmpty(query)) {
|
if (!TextUtils.isEmpty(query)) {
|
||||||
saveRecentSearch(query.toString());
|
saveRecentSearch(query.toString());
|
||||||
viewPager.setVisibility(View.VISIBLE);
|
binding.viewPager.setVisibility(View.VISIBLE);
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
searchHistoryContainer.setVisibility(View.GONE);
|
binding.searchHistoryContainer.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
||||||
searchDepictionsFragment.onQueryUpdated(query.toString());
|
searchDepictionsFragment.onQueryUpdated(query.toString());
|
||||||
|
|
@ -144,10 +134,10 @@ public class SearchActivity extends BaseActivity
|
||||||
else {
|
else {
|
||||||
//Open RecentSearchesFragment
|
//Open RecentSearchesFragment
|
||||||
recentSearchesFragment.updateRecentSearches();
|
recentSearchesFragment.updateRecentSearches();
|
||||||
viewPager.setVisibility(View.GONE);
|
binding.viewPager.setVisibility(View.GONE);
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.tabLayout.setVisibility(View.GONE);
|
||||||
setSearchHistoryFragment();
|
setSearchHistoryFragment();
|
||||||
searchHistoryContainer.setVisibility(View.VISIBLE);
|
binding.searchHistoryContainer.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,13 +205,13 @@ public class SearchActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int index) {
|
public void onMediaClicked(int index) {
|
||||||
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
|
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.tabLayout.setVisibility(View.GONE);
|
||||||
viewPager.setVisibility(View.GONE);
|
binding.viewPager.setVisibility(View.GONE);
|
||||||
mediaContainer.setVisibility(View.VISIBLE);
|
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||||
searchView.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
|
binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
|
||||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
|
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
|
||||||
|
|
@ -269,12 +259,12 @@ public class SearchActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||||
// back to search so show search toolbar and hide navigation toolbar
|
// back to search so show search toolbar and hide navigation toolbar
|
||||||
searchView.setVisibility(View.VISIBLE);//set the searchview
|
binding.searchBox.setVisibility(View.VISIBLE);//set the searchview
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
viewPager.setVisibility(View.VISIBLE);
|
binding.viewPager.setVisibility(View.VISIBLE);
|
||||||
mediaContainer.setVisibility(View.GONE);
|
binding.mediaContainer.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
toolbar.setVisibility(View.GONE);
|
binding.toolbarSearch.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
@ -284,15 +274,16 @@ public class SearchActivity extends BaseActivity
|
||||||
* @param query Recent Search Query
|
* @param query Recent Search Query
|
||||||
*/
|
*/
|
||||||
public void updateText(String query) {
|
public void updateText(String query) {
|
||||||
searchView.setQuery(query, true);
|
binding.searchBox.setQuery(query, true);
|
||||||
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
|
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
|
||||||
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
|
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
|
||||||
viewPager.requestFocus();
|
binding.viewPager.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void onDestroy() {
|
@Override protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
//Dispose the disposables when the activity is destroyed
|
//Dispose the disposables when the activity is destroyed
|
||||||
compositeDisposable.dispose();
|
compositeDisposable.dispose();
|
||||||
|
binding = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsInterface
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.get
|
import fr.free.nrw.commons.upload.structure.depictions.get
|
||||||
import fr.free.nrw.commons.wikidata.WikidataProperties
|
import fr.free.nrw.commons.wikidata.WikidataProperties
|
||||||
|
import fr.free.nrw.commons.wikidata.model.DataValue
|
||||||
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
||||||
|
import fr.free.nrw.commons.wikidata.model.Entities
|
||||||
|
import fr.free.nrw.commons.wikidata.model.Statement_partial
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import org.wikipedia.wikidata.DataValue
|
|
||||||
import org.wikipedia.wikidata.Entities
|
|
||||||
import org.wikipedia.wikidata.Statement_partial
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
|
|
@ -23,6 +21,7 @@ import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding;
|
||||||
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
|
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
|
||||||
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
|
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
|
||||||
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
|
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
|
||||||
|
|
@ -57,14 +56,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
||||||
@Inject
|
@Inject
|
||||||
DepictModel depictModel;
|
DepictModel depictModel;
|
||||||
private String wikidataItemName;
|
private String wikidataItemName;
|
||||||
@BindView(R.id.mediaContainer)
|
private ActivityWikidataItemDetailsBinding binding;
|
||||||
FrameLayout mediaContainer;
|
|
||||||
@BindView(R.id.tab_layout)
|
|
||||||
TabLayout tabLayout;
|
|
||||||
@BindView(R.id.viewPager)
|
|
||||||
ViewPager viewPager;
|
|
||||||
@BindView(R.id.toolbar)
|
|
||||||
Toolbar toolbar;
|
|
||||||
|
|
||||||
ViewPagerAdapter viewPagerAdapter;
|
ViewPagerAdapter viewPagerAdapter;
|
||||||
private DepictedItem wikidataItem;
|
private DepictedItem wikidataItem;
|
||||||
|
|
@ -72,19 +64,20 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_wikidata_item_details);
|
|
||||||
ButterKnife.bind(this);
|
binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
compositeDisposable = new CompositeDisposable();
|
compositeDisposable = new CompositeDisposable();
|
||||||
supportFragmentManager = getSupportFragmentManager();
|
supportFragmentManager = getSupportFragmentManager();
|
||||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||||
viewPager.setAdapter(viewPagerAdapter);
|
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||||
viewPager.setOffscreenPageLimit(2);
|
binding.viewPager.setOffscreenPageLimit(2);
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
|
|
||||||
final DepictedItem depictedItem = getIntent().getParcelableExtra(
|
final DepictedItem depictedItem = getIntent().getParcelableExtra(
|
||||||
WikidataConstants.BOOKMARKS_ITEMS);
|
WikidataConstants.BOOKMARKS_ITEMS);
|
||||||
wikidataItem = depictedItem;
|
wikidataItem = depictedItem;
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
setTabs();
|
setTabs();
|
||||||
setPageTitle();
|
setPageTitle();
|
||||||
|
|
@ -137,7 +130,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
||||||
fragmentList.add(parentDepictionsFragment);
|
fragmentList.add(parentDepictionsFragment);
|
||||||
titleList.add(getResources().getString(R.string.title_for_parent_classes));
|
titleList.add(getResources().getString(R.string.title_for_parent_classes));
|
||||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||||
viewPager.setOffscreenPageLimit(2);
|
binding.viewPager.setOffscreenPageLimit(2);
|
||||||
viewPagerAdapter.notifyDataSetChanged();
|
viewPagerAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -148,12 +141,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onMediaClicked(int position) {
|
public void onMediaClicked(int position) {
|
||||||
tabLayout.setVisibility(View.GONE);
|
binding.tabLayout.setVisibility(View.GONE);
|
||||||
viewPager.setVisibility(View.GONE);
|
binding.viewPager.setVisibility(View.GONE);
|
||||||
mediaContainer.setVisibility(View.VISIBLE);
|
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||||
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
||||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||||
mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
|
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
||||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||||
supportFragmentManager
|
supportFragmentManager
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
|
|
@ -183,9 +176,9 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||||
viewPager.setVisibility(View.VISIBLE);
|
binding.viewPager.setVisibility(View.VISIBLE);
|
||||||
mediaContainer.setVisibility(View.GONE);
|
binding.mediaContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ public class ExploreMapCalls {
|
||||||
/**
|
/**
|
||||||
* Calls method to query Commons for uploads around a location
|
* Calls method to query Commons for uploads around a location
|
||||||
*
|
*
|
||||||
* @param curLatLng coordinates of search location
|
* @param currentLatLng coordinates of search location
|
||||||
* @return list of places obtained
|
* @return list of places obtained
|
||||||
*/
|
*/
|
||||||
List<Media> callCommonsQuery(final LatLng curLatLng) {
|
List<Media> callCommonsQuery(final LatLng currentLatLng) {
|
||||||
String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude();
|
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
|
||||||
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
|
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,34 @@
|
||||||
package fr.free.nrw.commons.explore.map;
|
package fr.free.nrw.commons.explore.map;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraUpdate;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ExploreMapContract {
|
public class ExploreMapContract {
|
||||||
|
|
||||||
interface View {
|
interface View {
|
||||||
boolean isNetworkConnectionEstablished();
|
boolean isNetworkConnectionEstablished();
|
||||||
void populatePlaces(LatLng curlatLng,LatLng searchLatLng);
|
void populatePlaces(LatLng curlatLng);
|
||||||
void checkPermissionsAndPerformAction();
|
void askForLocationPermission();
|
||||||
void recenterMap(LatLng curLatLng);
|
void recenterMap(LatLng curLatLng);
|
||||||
void showLocationOffDialog();
|
|
||||||
void openLocationSettings();
|
|
||||||
void hideBottomDetailsSheet();
|
void hideBottomDetailsSheet();
|
||||||
void displayBottomSheetWithInfo(Marker marker);
|
LatLng getMapCenter();
|
||||||
void addOnCameraMoveListener();
|
LatLng getMapFocus();
|
||||||
|
LatLng getLastMapFocus();
|
||||||
|
void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers);
|
||||||
|
void clearAllMarkers();
|
||||||
void addSearchThisAreaButtonAction();
|
void addSearchThisAreaButtonAction();
|
||||||
void setSearchThisAreaButtonVisibility(boolean isVisible);
|
void setSearchThisAreaButtonVisibility(boolean isVisible);
|
||||||
void setProgressBarVisibility(boolean isVisible);
|
void setProgressBarVisibility(boolean isVisible);
|
||||||
boolean isDetailsBottomSheetVisible();
|
boolean isDetailsBottomSheetVisible();
|
||||||
boolean isSearchThisAreaButtonVisible();
|
boolean isSearchThisAreaButtonVisible();
|
||||||
void addCurrentLocationMarker(LatLng curLatLng);
|
|
||||||
void updateMapToTrackPosition(LatLng curLatLng);
|
|
||||||
Context getContext();
|
Context getContext();
|
||||||
LatLng getCameraTarget();
|
|
||||||
void centerMapToPlace(Place placeToCenter);
|
|
||||||
LatLng getLastLocation();
|
LatLng getLastLocation();
|
||||||
com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation();
|
|
||||||
boolean isCurrentLocationMarkerVisible();
|
|
||||||
void setProjectorLatLngBounds();
|
|
||||||
void disableFABRecenter();
|
void disableFABRecenter();
|
||||||
void enableFABRecenter();
|
void enableFABRecenter();
|
||||||
void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker);
|
|
||||||
void setMapBoundaries(CameraUpdate cameaUpdate);
|
|
||||||
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
|
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
|
||||||
boolean backButtonClicked();
|
boolean backButtonClicked();
|
||||||
}
|
}
|
||||||
|
|
@ -51,9 +40,6 @@ public class ExploreMapContract {
|
||||||
void detachView();
|
void detachView();
|
||||||
void setActionListeners(JsonKvStore applicationKvStore);
|
void setActionListeners(JsonKvStore applicationKvStore);
|
||||||
boolean backButtonClicked();
|
boolean backButtonClicked();
|
||||||
void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng);
|
|
||||||
void markerUnselected();
|
|
||||||
void markerSelected(Marker marker);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,11 @@ import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.bumptech.glide.request.target.CustomTarget;
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
|
||||||
import fr.free.nrw.commons.MapController;
|
import fr.free.nrw.commons.MapController;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
import fr.free.nrw.commons.utils.LocationUtils;
|
import fr.free.nrw.commons.utils.LocationUtils;
|
||||||
|
|
@ -33,6 +31,7 @@ import javax.inject.Inject;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class ExploreMapController extends MapController {
|
public class ExploreMapController extends MapController {
|
||||||
|
|
||||||
private final ExploreMapCalls exploreMapCalls;
|
private final ExploreMapCalls exploreMapCalls;
|
||||||
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
||||||
public LatLng currentLocation; // current location of user
|
public LatLng currentLocation; // current location of user
|
||||||
|
|
@ -46,13 +45,18 @@ public class ExploreMapController extends MapController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes location as parameter and returns ExplorePlaces info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
|
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
|
||||||
* @param curLatLng is current geolocation
|
* explorePlaceList and boundaryCoordinates
|
||||||
|
*
|
||||||
|
* @param currentLatLng is current geolocation
|
||||||
* @param searchLatLng is the location that we want to search around
|
* @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 checkingAroundCurrentLocation is a boolean flag. True if we want to check around
|
||||||
* @return explorePlacesInfo info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
|
* current location, false if another location
|
||||||
|
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
|
||||||
|
* boundaryCoordinates
|
||||||
*/
|
*/
|
||||||
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrentLocation) {
|
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng,
|
||||||
|
boolean checkingAroundCurrentLocation) {
|
||||||
|
|
||||||
if (searchLatLng == null) {
|
if (searchLatLng == null) {
|
||||||
Timber.d("Loading attractions explore map, but search is null");
|
Timber.d("Loading attractions explore map, but search is null");
|
||||||
|
|
@ -61,7 +65,7 @@ public class ExploreMapController extends MapController {
|
||||||
|
|
||||||
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
|
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
|
||||||
try {
|
try {
|
||||||
explorePlacesInfo.curLatLng = curLatLng;
|
explorePlacesInfo.currentLatLng = currentLatLng;
|
||||||
latestSearchLocation = searchLatLng;
|
latestSearchLocation = searchLatLng;
|
||||||
|
|
||||||
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
|
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
|
||||||
|
|
@ -74,18 +78,23 @@ public class ExploreMapController extends MapController {
|
||||||
Timber.d("Sorting places by distance...");
|
Timber.d("Sorting places by distance...");
|
||||||
final Map<Media, Double> distances = new HashMap<>();
|
final Map<Media, Double> distances = new HashMap<>();
|
||||||
for (Media media : mediaList) {
|
for (Media media : mediaList) {
|
||||||
distances.put(media, computeDistanceBetween(media.getCoordinates(), searchLatLng));
|
distances.put(media,
|
||||||
|
computeDistanceBetween(media.getCoordinates(), searchLatLng));
|
||||||
// Find boundaries with basic find max approach
|
// Find boundaries with basic find max approach
|
||||||
if (media.getCoordinates().getLatitude() < boundaryCoordinates[0].getLatitude()) {
|
if (media.getCoordinates().getLatitude()
|
||||||
|
< boundaryCoordinates[0].getLatitude()) {
|
||||||
boundaryCoordinates[0] = media.getCoordinates();
|
boundaryCoordinates[0] = media.getCoordinates();
|
||||||
}
|
}
|
||||||
if (media.getCoordinates().getLatitude() > boundaryCoordinates[1].getLatitude()) {
|
if (media.getCoordinates().getLatitude()
|
||||||
|
> boundaryCoordinates[1].getLatitude()) {
|
||||||
boundaryCoordinates[1] = media.getCoordinates();
|
boundaryCoordinates[1] = media.getCoordinates();
|
||||||
}
|
}
|
||||||
if (media.getCoordinates().getLongitude() < boundaryCoordinates[2].getLongitude()) {
|
if (media.getCoordinates().getLongitude()
|
||||||
|
< boundaryCoordinates[2].getLongitude()) {
|
||||||
boundaryCoordinates[2] = media.getCoordinates();
|
boundaryCoordinates[2] = media.getCoordinates();
|
||||||
}
|
}
|
||||||
if (media.getCoordinates().getLongitude() > boundaryCoordinates[3].getLongitude()) {
|
if (media.getCoordinates().getLongitude()
|
||||||
|
> boundaryCoordinates[3].getLongitude()) {
|
||||||
boundaryCoordinates[3] = media.getCoordinates();
|
boundaryCoordinates[3] = media.getCoordinates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +105,8 @@ public class ExploreMapController extends MapController {
|
||||||
|
|
||||||
// Sets latestSearchRadius to maximum distance among boundaries and search location
|
// Sets latestSearchRadius to maximum distance among boundaries and search location
|
||||||
for (LatLng bound : boundaryCoordinates) {
|
for (LatLng bound : boundaryCoordinates) {
|
||||||
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(bound).distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(latestSearchLocation));
|
double distance = LocationUtils.calculateDistance(bound.getLatitude(),
|
||||||
|
bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude());
|
||||||
if (distance > latestSearchRadius) {
|
if (distance > latestSearchRadius) {
|
||||||
latestSearchRadius = distance;
|
latestSearchRadius = distance;
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +115,7 @@ public class ExploreMapController extends MapController {
|
||||||
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
|
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
|
||||||
if (checkingAroundCurrentLocation) {
|
if (checkingAroundCurrentLocation) {
|
||||||
currentLocationSearchRadius = latestSearchRadius;
|
currentLocationSearchRadius = latestSearchRadius;
|
||||||
currentLocation = curLatLng;
|
currentLocation = currentLatLng;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
@ -115,42 +125,42 @@ public class ExploreMapController extends MapController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads attractions from location for map view, we need to return places in Place data type
|
* Loads attractions from location for map view, we need to return places in Place data type
|
||||||
|
*
|
||||||
* @return baseMarkerOptions list that holds nearby places with their icons
|
* @return baseMarkerOptions list that holds nearby places with their icons
|
||||||
*/
|
*/
|
||||||
public static List<NearbyBaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
|
public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
|
||||||
LatLng curLatLng,
|
LatLng currentLatLng,
|
||||||
final List<Place> placeList,
|
final List<Place> placeList,
|
||||||
Context context,
|
Context context,
|
||||||
NearbyBaseMarkerThumbCallback callback,
|
NearbyBaseMarkerThumbCallback callback,
|
||||||
Marker selectedMarker,
|
|
||||||
boolean shouldTrackPosition,
|
|
||||||
ExplorePlacesInfo explorePlacesInfo) {
|
ExplorePlacesInfo explorePlacesInfo) {
|
||||||
List<NearbyBaseMarker> baseMarkerOptions = new ArrayList<>();
|
List<BaseMarker> baseMarkerList = new ArrayList<>();
|
||||||
|
|
||||||
if (placeList == null) {
|
if (placeList == null) {
|
||||||
return baseMarkerOptions;
|
return baseMarkerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
VectorDrawableCompat vectorDrawable = null;
|
VectorDrawableCompat vectorDrawable = null;
|
||||||
try {
|
try {
|
||||||
vectorDrawable = VectorDrawableCompat.create(
|
vectorDrawable = VectorDrawableCompat.create(
|
||||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme());
|
context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme());
|
||||||
|
|
||||||
} catch (Resources.NotFoundException e) {
|
} catch (Resources.NotFoundException e) {
|
||||||
// ignore when running tests.
|
// ignore when running tests.
|
||||||
}
|
}
|
||||||
if (vectorDrawable != null) {
|
if (vectorDrawable != null) {
|
||||||
for (Place explorePlace : placeList) {
|
for (Place explorePlace : placeList) {
|
||||||
final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
final BaseMarker baseMarker = new BaseMarker();
|
||||||
String distance = formatDistanceBetween(curLatLng, explorePlace.location);
|
String distance = formatDistanceBetween(currentLatLng, explorePlace.location);
|
||||||
explorePlace.setDistance(distance);
|
explorePlace.setDistance(distance);
|
||||||
|
|
||||||
nearbyBaseMarker.title(explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
|
baseMarker.setTitle(
|
||||||
nearbyBaseMarker.position(
|
explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
|
||||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
baseMarker.setPosition(
|
||||||
|
new fr.free.nrw.commons.location.LatLng(
|
||||||
explorePlace.location.getLatitude(),
|
explorePlace.location.getLatitude(),
|
||||||
explorePlace.location.getLongitude()));
|
explorePlace.location.getLongitude(), 0));
|
||||||
nearbyBaseMarker.place(explorePlace);
|
baseMarker.setPlace(explorePlace);
|
||||||
|
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
|
|
@ -160,12 +170,15 @@ public class ExploreMapController extends MapController {
|
||||||
.into(new CustomTarget<Bitmap>() {
|
.into(new CustomTarget<Bitmap>() {
|
||||||
// We add icons to markers when bitmaps are ready
|
// We add icons to markers when bitmaps are ready
|
||||||
@Override
|
@Override
|
||||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
public void onResourceReady(@NonNull Bitmap resource,
|
||||||
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromBitmap(
|
@Nullable Transition<? super Bitmap> transition) {
|
||||||
ImageUtils.addRedBorder(resource, 6, context)));
|
baseMarker.setIcon(
|
||||||
baseMarkerOptions.add(nearbyBaseMarker);
|
ImageUtils.addRedBorder(resource, 6, context));
|
||||||
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
baseMarkerList.add(baseMarker);
|
||||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
|
if (baseMarkerList.size()
|
||||||
|
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||||
|
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
||||||
|
explorePlacesInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,20 +190,24 @@ public class ExploreMapController extends MapController {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
|
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
|
||||||
super.onLoadFailed(errorDrawable);
|
super.onLoadFailed(errorDrawable);
|
||||||
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromResource(R.drawable.image_placeholder_96));
|
baseMarker.fromResource(context, R.drawable.image_placeholder_96);
|
||||||
baseMarkerOptions.add(nearbyBaseMarker);
|
baseMarkerList.add(baseMarker);
|
||||||
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
if (baseMarkerList.size()
|
||||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
|
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||||
|
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
||||||
|
explorePlacesInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return baseMarkerOptions;
|
return baseMarkerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NearbyBaseMarkerThumbCallback {
|
interface NearbyBaseMarkerThumbCallback {
|
||||||
|
|
||||||
// Callback to notify thumbnails of explore markers are added as icons and ready
|
// Callback to notify thumbnails of explore markers are added as icons and ready
|
||||||
void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition);
|
void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
|
||||||
|
ExplorePlacesInfo explorePlacesInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,10 +4,9 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChange
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
|
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
|
||||||
|
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
|
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
|
|
||||||
import fr.free.nrw.commons.MapController;
|
import fr.free.nrw.commons.MapController;
|
||||||
import fr.free.nrw.commons.MapController.ExplorePlacesInfo;
|
import fr.free.nrw.commons.MapController.ExplorePlacesInfo;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||||
|
|
@ -15,8 +14,6 @@ import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThum
|
||||||
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.LocationChangeType;
|
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
||||||
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
|
|
||||||
import fr.free.nrw.commons.utils.LocationUtils;
|
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -25,10 +22,10 @@ import timber.log.Timber;
|
||||||
public class ExploreMapPresenter
|
public class ExploreMapPresenter
|
||||||
implements ExploreMapContract.UserActions,
|
implements ExploreMapContract.UserActions,
|
||||||
NearbyBaseMarkerThumbCallback {
|
NearbyBaseMarkerThumbCallback {
|
||||||
|
|
||||||
BookmarkLocationsDao bookmarkLocationDao;
|
BookmarkLocationsDao bookmarkLocationDao;
|
||||||
private boolean isNearbyLocked;
|
private boolean isNearbyLocked;
|
||||||
private boolean placesLoadedOnce;
|
private LatLng currentLatLng;
|
||||||
private LatLng curLatLng;
|
|
||||||
private ExploreMapController exploreMapController;
|
private ExploreMapController exploreMapController;
|
||||||
|
|
||||||
private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy
|
private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy
|
||||||
|
|
@ -54,7 +51,7 @@ public class ExploreMapPresenter
|
||||||
);
|
);
|
||||||
private ExploreMapContract.View exploreMapFragmentView = DUMMY;
|
private ExploreMapContract.View exploreMapFragmentView = DUMMY;
|
||||||
|
|
||||||
public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao){
|
public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao) {
|
||||||
this.bookmarkLocationDao = bookmarkLocationDao;
|
this.bookmarkLocationDao = bookmarkLocationDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,14 +68,6 @@ public class ExploreMapPresenter
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LatLng lastLocation = exploreMapFragmentView.getLastLocation();
|
|
||||||
curLatLng = lastLocation;
|
|
||||||
|
|
||||||
if (curLatLng == null) {
|
|
||||||
Timber.d("Skipping update of nearby places as location is unavailable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Significant changed - Markers and current location will be updated together
|
* Significant changed - Markers and current location will be updated together
|
||||||
* Slightly changed - Only current position marker will be updated
|
* Slightly changed - Only current position marker will be updated
|
||||||
|
|
@ -87,24 +76,21 @@ public class ExploreMapPresenter
|
||||||
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
|
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
|
||||||
lockUnlockNearby(true);
|
lockUnlockNearby(true);
|
||||||
exploreMapFragmentView.setProgressBarVisibility(true);
|
exploreMapFragmentView.setProgressBarVisibility(true);
|
||||||
exploreMapFragmentView.populatePlaces(curLatLng, lastLocation);
|
exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapCenter());
|
||||||
|
|
||||||
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
|
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
|
||||||
Timber.d("SEARCH_CUSTOM_AREA");
|
Timber.d("SEARCH_CUSTOM_AREA");
|
||||||
lockUnlockNearby(true);
|
lockUnlockNearby(true);
|
||||||
exploreMapFragmentView.setProgressBarVisibility(true);
|
exploreMapFragmentView.setProgressBarVisibility(true);
|
||||||
exploreMapFragmentView.populatePlaces(curLatLng, exploreMapFragmentView.getCameraTarget());
|
exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus());
|
||||||
} else { // Means location changed slightly, ie user is walking or driving.
|
} else { // Means location changed slightly, ie user is walking or driving.
|
||||||
Timber.d("Means location changed slightly");
|
Timber.d("Means location changed slightly");
|
||||||
if (exploreMapFragmentView.isCurrentLocationMarkerVisible()){ // Means user wants to see their live location
|
|
||||||
exploreMapFragmentView.recenterMap(curLatLng);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nearby updates takes time, since they are network operations. During update time, we don't
|
* Nearby updates takes time, since they are network operations. During update time, we don't
|
||||||
* want to get any other calls from user. So locking nearby.
|
* want to get any other calls from user. So locking nearby.
|
||||||
|
*
|
||||||
* @param isNearbyLocked true means lock, false means unlock
|
* @param isNearbyLocked true means lock, false means unlock
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -133,7 +119,7 @@ public class ExploreMapPresenter
|
||||||
@Override
|
@Override
|
||||||
public void setActionListeners(JsonKvStore applicationKvStore) {
|
public void setActionListeners(JsonKvStore applicationKvStore) {
|
||||||
exploreMapFragmentView.setFABRecenterAction(v -> {
|
exploreMapFragmentView.setFABRecenterAction(v -> {
|
||||||
exploreMapFragmentView.recenterMap(curLatLng);
|
exploreMapFragmentView.recenterMap(currentLatLng);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -143,29 +129,9 @@ public class ExploreMapPresenter
|
||||||
return exploreMapFragmentView.backButtonClicked();
|
return exploreMapFragmentView.backButtonClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng) {
|
|
||||||
exploreMapFragmentView.setProjectorLatLngBounds();
|
|
||||||
// If our nearby markers are calculated at least once
|
|
||||||
if (exploreMapController.latestSearchLocation != null) {
|
|
||||||
double distance = latLng.distanceTo
|
|
||||||
(LocationUtils.commonsLatLngToMapBoxLatLng(exploreMapController.latestSearchLocation));
|
|
||||||
if (exploreMapFragmentView.isNetworkConnectionEstablished()) {
|
|
||||||
if (distance > exploreMapController.latestSearchRadius && exploreMapController.latestSearchRadius != 0) {
|
|
||||||
exploreMapFragmentView.setSearchThisAreaButtonVisibility(true);
|
|
||||||
} else {
|
|
||||||
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMapReady(ExploreMapController exploreMapController) {
|
public void onMapReady(ExploreMapController exploreMapController) {
|
||||||
this.exploreMapController = exploreMapController;
|
this.exploreMapController = exploreMapController;
|
||||||
exploreMapFragmentView.addSearchThisAreaButtonAction();
|
if (null != exploreMapFragmentView) {
|
||||||
if(null != exploreMapFragmentView) {
|
|
||||||
exploreMapFragmentView.addSearchThisAreaButtonAction();
|
exploreMapFragmentView.addSearchThisAreaButtonAction();
|
||||||
initializeMapOperations();
|
initializeMapOperations();
|
||||||
}
|
}
|
||||||
|
|
@ -174,69 +140,48 @@ public class ExploreMapPresenter
|
||||||
public void initializeMapOperations() {
|
public void initializeMapOperations() {
|
||||||
lockUnlockNearby(false);
|
lockUnlockNearby(false);
|
||||||
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
|
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
exploreMapFragmentView.addSearchThisAreaButtonAction();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<ExplorePlacesInfo> loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrent) {
|
public Observable<ExplorePlacesInfo> loadAttractionsFromLocation(LatLng currentLatLng,
|
||||||
|
LatLng searchLatLng, boolean checkingAroundCurrent) {
|
||||||
return Observable
|
return Observable
|
||||||
.fromCallable(() -> exploreMapController
|
.fromCallable(() -> exploreMapController
|
||||||
.loadAttractionsFromLocation(curLatLng, searchLatLng,checkingAroundCurrent));
|
.loadAttractionsFromLocation(currentLatLng, searchLatLng, checkingAroundCurrent));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates places for custom location, should be used for finding nearby places around a
|
* Populates places for custom location, should be used for finding nearby places around a
|
||||||
* location where you are not at.
|
* location where you are not at.
|
||||||
|
*
|
||||||
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
|
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
|
||||||
*/
|
*/
|
||||||
public void updateMapMarkers(
|
public void updateMapMarkers(
|
||||||
MapController.ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
|
MapController.ExplorePlacesInfo explorePlacesInfo) {
|
||||||
exploreMapFragmentView.setMapBoundaries(CameraUpdateFactory.newLatLngBounds(getLatLngBounds(explorePlacesInfo.boundaryCoordinates), 50));
|
if (explorePlacesInfo.mediaList != null) {
|
||||||
prepareNearbyBaseMarkers(explorePlacesInfo, selectedMarker, shouldTrackPosition);
|
prepareNearbyBaseMarkers(explorePlacesInfo);
|
||||||
|
} else {
|
||||||
|
lockUnlockNearby(false); // So that new location updates wont come
|
||||||
|
exploreMapFragmentView.setProgressBarVisibility(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
|
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) {
|
||||||
exploreMapController
|
exploreMapController
|
||||||
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.curLatLng, // Curlatlang will be used to calculate distances
|
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
|
||||||
|
// Curlatlang will be used to calculate distances
|
||||||
explorePlacesInfo.explorePlaceList,
|
explorePlacesInfo.explorePlaceList,
|
||||||
exploreMapFragmentView.getContext(),
|
exploreMapFragmentView.getContext(),
|
||||||
this,
|
this,
|
||||||
selectedMarker,
|
|
||||||
shouldTrackPosition,
|
|
||||||
explorePlacesInfo);
|
explorePlacesInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
|
public void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
|
||||||
if(null != exploreMapFragmentView) {
|
ExplorePlacesInfo explorePlacesInfo) {
|
||||||
exploreMapFragmentView.addNearbyMarkersToMapBoxMap(baseMarkers, selectedMarker);
|
if (null != exploreMapFragmentView) {
|
||||||
exploreMapFragmentView.addCurrentLocationMarker(explorePlacesInfo.curLatLng);
|
exploreMapFragmentView.addMarkersToMap(baseMarkers);
|
||||||
if(shouldTrackPosition){
|
|
||||||
exploreMapFragmentView.updateMapToTrackPosition(explorePlacesInfo.curLatLng);
|
|
||||||
}
|
|
||||||
lockUnlockNearby(false); // So that new location updates wont come
|
lockUnlockNearby(false); // So that new location updates wont come
|
||||||
exploreMapFragmentView.setProgressBarVisibility(false);
|
exploreMapFragmentView.setProgressBarVisibility(false);
|
||||||
handleCenteringTaskIfAny();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LatLngBounds getLatLngBounds(LatLng[] boundaries) {
|
|
||||||
LatLngBounds latLngBounds = new LatLngBounds.Builder()
|
|
||||||
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[0]))
|
|
||||||
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[1]))
|
|
||||||
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[2]))
|
|
||||||
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[3]))
|
|
||||||
.build();
|
|
||||||
return latLngBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some centering task may need to wait for map to be ready, if they are requested before
|
|
||||||
* map is ready. So we will remember it when the map is ready
|
|
||||||
*/
|
|
||||||
private void handleCenteringTaskIfAny() {
|
|
||||||
if (!placesLoadedOnce) {
|
|
||||||
placesLoadedOnce = true;
|
|
||||||
exploreMapFragmentView.centerMapToPlace(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +190,7 @@ public class ExploreMapPresenter
|
||||||
// Lock map operations during search this area operation
|
// Lock map operations during search this area operation
|
||||||
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
|
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
|
||||||
|
|
||||||
if (searchCloseToCurrentLocation()){
|
if (searchCloseToCurrentLocation()) {
|
||||||
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
|
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
|
||||||
} else {
|
} else {
|
||||||
updateMap(SEARCH_CUSTOM_AREA);
|
updateMap(SEARCH_CUSTOM_AREA);
|
||||||
|
|
@ -254,40 +199,29 @@ public class ExploreMapPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if search this area button is used around our current location, so that
|
* Returns true if search this area button is used around our current location, so that we can
|
||||||
* we can continue following our current location again
|
* continue following our current location again
|
||||||
|
*
|
||||||
* @return Returns true if search this area button is used around our current location
|
* @return Returns true if search this area button is used around our current location
|
||||||
*/
|
*/
|
||||||
public boolean searchCloseToCurrentLocation() {
|
public boolean searchCloseToCurrentLocation() {
|
||||||
if (null == exploreMapFragmentView.getLastFocusLocation() || exploreMapController.latestSearchRadius == 0) {
|
if (null == exploreMapFragmentView.getLastMapFocus()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(exploreMapFragmentView.getCameraTarget())
|
|
||||||
.distanceTo(exploreMapFragmentView.getLastFocusLocation());
|
Location mylocation = new Location("");
|
||||||
if (distance > exploreMapController.currentLocationSearchRadius * 3 / 4) {
|
Location dest_location = new Location("");
|
||||||
|
dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude());
|
||||||
|
dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude());
|
||||||
|
mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude());
|
||||||
|
mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude());
|
||||||
|
Float distance = mylocation.distanceTo(dest_location);
|
||||||
|
|
||||||
|
if (distance > 2000.0 * 3 / 4) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markerUnselected() {
|
|
||||||
exploreMapFragmentView.hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void markerSelected(Marker marker) {
|
|
||||||
exploreMapFragmentView.displayBottomSheetWithInfo(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean areLocationsClose(LatLng cameraTarget, LatLng lastKnownLocation) {
|
|
||||||
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(cameraTarget)
|
|
||||||
.distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(lastKnownLocation));
|
|
||||||
if (distance > exploreMapController.currentLocationSearchRadius * 3 / 4) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue