mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-29 13:53:54 +01:00
Merge branch 'main' into replace_toasts
This commit is contained in:
commit
759ed34cf2
922 changed files with 29317 additions and 44373 deletions
18
.github/workflows/android.yml
vendored
18
.github/workflows/android.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Android CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
|
|
@ -12,17 +12,17 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v2.5.0
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 11
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache packages
|
||||
id: cache-packages
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
|
||||
- name: AVD cache
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
|
|
@ -89,7 +89,7 @@ jobs:
|
|||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||
|
||||
- name: Upload betaDebug APK
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: betaDebugAPK
|
||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||
|
||||
- name: Upload prodDebug APK
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prodDebugAPK
|
||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -43,3 +43,7 @@ app/src/main/jniLibs
|
|||
#https://docs.opencv.org/3.3.0/
|
||||
/libraries/opencv/javadoc/
|
||||
captures/*
|
||||
|
||||
# Test and other output
|
||||
app/jacoco.exec
|
||||
app/CommonsContributions
|
||||
17
.idea/codeStyles/Project.xml
generated
17
.idea/codeStyles/Project.xml
generated
|
|
@ -39,21 +39,18 @@
|
|||
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
|
||||
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
|
||||
</Objective-C>
|
||||
<Objective-C-extensions>
|
||||
<extensions>
|
||||
<pair source="cc" header="h" fileNamingConvention="NONE" />
|
||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
<Python>
|
||||
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
|
||||
</Python>
|
||||
<TypeScriptCodeStyleSettings>
|
||||
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<XML>
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
<files>
|
||||
<extensions>
|
||||
<pair source="cc" header="h" fileNamingConvention="NONE" />
|
||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</files>
|
||||
<codeStyleSettings language="CSS">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
|
|
@ -318,9 +315,7 @@
|
|||
<codeStyleSettings language="protobuf">
|
||||
<option name="RIGHT_MARGIN" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
|
|
|
|||
5
.mailmap
Normal file
5
.mailmap
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# See: https://git-scm.com/docs/git-shortlog#_mapping_authors
|
||||
#
|
||||
Brooke Vibber <bvibber@wikimedia.org>
|
||||
Brooke Vibber <bvibber@wikimedia.org> <brion@wikimedia.org>
|
||||
Brooke Vibber <bvibber@wikimedia.org> <brion@pobox.com>
|
||||
149
CHANGELOG.md
149
CHANGELOG.md
|
|
@ -1,5 +1,154 @@
|
|||
# Wikimedia Commons for Android
|
||||
|
||||
## v5.0.2
|
||||
|
||||
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
|
||||
same category and depictions.
|
||||
- Show Wikidata description on currently active Nearby pin to provide more useful information.
|
||||
- Improve the visibility of map markers by dynamically adjusting their colors based on the app's
|
||||
theme. The map markers will now appear lighter when the app is in dark mode and darker when the
|
||||
app is in light mode. This change aims to enhance marker visibility and improve the overall user
|
||||
experience.
|
||||
- Added information on where user feedback is posted, helping users track existing feedback and
|
||||
monitor their own submissions.
|
||||
- Enhanced the edit location screen of the upload screen by centering the map on the picture's
|
||||
location from metadata when editing, or on the device's GPS location if metadata is unavailable,
|
||||
improving accuracy and user experience.
|
||||
- Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a
|
||||
recently uploaded image, enhancing clarity and user experience.
|
||||
- Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user
|
||||
experience by showing loading progress until the image is fully loaded.
|
||||
- Fixed an issue where caption and description fields would intermittently disappear when using
|
||||
voice input, ensuring text remains visible and stable across all entries.
|
||||
- Fixed a crash that occurred when attempting to remove multiple instances of caption/description
|
||||
fields after initially adding them.
|
||||
- Improve the text in the prompt shown when skipping login to sound more natural.
|
||||
- Modified feedback addition logic to append new sections at the bottom of the page, ensuring
|
||||
auto-archiving of sections functions correctly on the feedback page.
|
||||
- Resolved issue where the app failed to clear cookies upon logout.
|
||||
|
||||
## v5.0.1
|
||||
|
||||
Same as v5.0.0 except this fixes some R8 rules to ensure that the release
|
||||
variants of the app work as intended.
|
||||
|
||||
## v5.0.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Redesigned the map feature to **replace Mapbox with the osmdroid library**.
|
||||
Key elements like pin visualization and user-centered display are still
|
||||
included in this redesign. This is done to guard against possible misuse of
|
||||
the Mapbox token and, more crucially, to keep the app from becoming dependent
|
||||
on a service that charges for usage but offers a free tier.
|
||||
|
||||
With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org).
|
||||
- Add the ability to **export locations of nearby missing pictures in GPX and
|
||||
KML formats**. This allows users to browse the locations with desired radius
|
||||
for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing
|
||||
accessibility and offline functionality.
|
||||
- **Limited the uploads via the custom image picker** to a maximum of 20.
|
||||
- Added two menu choices for **transparent image backgrounds**, giving users the
|
||||
option of either a black or white background, increasing adaptability to
|
||||
various theme settings.
|
||||
|
||||
User customization option has been provided with the
|
||||
ability to save background color selections permanently on a per image basis.
|
||||
- Implemented functionality to **automatically resume uploads** that become
|
||||
stuck due to app termination or device reboot.
|
||||
- Added a **compass arrow in the Nearby banner** shown in the "Contributions"
|
||||
screen to guide users towards the nearest item, thus providing the missing
|
||||
directional cues. The arrow dynamically adjusts based on device rotation,
|
||||
aligning with the calculated bearing towards the target location. Further,
|
||||
the distance and direction are updated as the user moves.
|
||||
- Implemented **voice input feature** for caption and description fields,
|
||||
enabling users to dictate text directly into these fields.
|
||||
- Improved various flows in the app to **redirect users to the login page** and
|
||||
display a persistent message **if their session becomes invalid** due to a
|
||||
password change, enhancing user guidance and security measures.
|
||||
|
||||
### Revamps and refactorings
|
||||
|
||||
- **Revamped initial upload screen layout and the description edit screen layout**
|
||||
for enhanced user experience and ensuring better symmetry in the design.
|
||||
- **Replaced Butterknife with ViewBinding** in various places of the app.
|
||||
- Transferred essential code from **the redundant data-client module** to the
|
||||
main Commons app code, enabling its integration and facilitating the removal
|
||||
of the redundant module. Further, convert various parts of the code to Kotlin.
|
||||
- **Revamped the various location permission flows** to ensure consistency for
|
||||
the sake of a better user experience.
|
||||
|
||||
### Bug fixes and various changes
|
||||
|
||||
- Resolved an issue where paused uploads that were subsequently cancelled were
|
||||
still being uploaded.
|
||||
- Fixed an issue where some user information such as upload count were not
|
||||
displayed in the "Contributions" and "Profile" screens.
|
||||
- Fixed the long-standing broken *"Picture of the Day" widget* to restore its
|
||||
usability.
|
||||
- Resolved an issue where some categories were hidden at the top of Upload
|
||||
Wizard suggestions.
|
||||
- Resolved an issue where there was a grey empty screen at Upload wizard when
|
||||
the app was denied the files permission.
|
||||
- Implemented logic to bypass media in Peer Review if the current reviewer is
|
||||
also the user who uploaded the media.
|
||||
- Corrected arrow image behaviour in the first upload screen: now displays down
|
||||
arrow when details card is fully visible, aligning with expected user
|
||||
interaction.
|
||||
- Updated app icon to improve visibility and recognition on F-Droid.
|
||||
- Fixed issue causing all pictures to disappear and activity to reload fully in
|
||||
the custom image selector after marking a picture as 'not for upload', now
|
||||
ensuring only the selected picture is removed as expected.
|
||||
|
||||
What's listed here is only a subset of all the changes. Check the full-list of
|
||||
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0).
|
||||
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0)
|
||||
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||
|
||||
## v4.2.1
|
||||
|
||||
- Provide the ability to edit an image to losslessly rotate it while uploading
|
||||
- Fix a bug in v4.2.0 where the nearby places were not loading
|
||||
- Fix a bug where editing depictions was showing a progress bar indefinitely
|
||||
- In the upload screen, use different map icons to indicate if image is being uploaded with location
|
||||
metadata
|
||||
- For nearby uploads, it is no longer possible to deselect the item's category and depiction
|
||||
- The Mapbox account key used by the app has been changed
|
||||
- Category search now shows exact matches without any discrepancies
|
||||
- Various bug and crash fixes
|
||||
|
||||
## v4.2.0
|
||||
- Dark mode colour improvements
|
||||
- Enhancements done to address location metadata loss including the metadata loss that occurs in
|
||||
latest Android versions
|
||||
- Enhancements done to address the issue where uploads get stuck in queued state
|
||||
- Fix the inability to upload via the in-app camera option
|
||||
- Provide the ability to optionally include location metadata for in-app camera uploads in case the
|
||||
device camera app does not provide location metadata
|
||||
- Use geo location URL that works consistently across all map applications
|
||||
- Fix crash when clicking on location target icon while trying to edit the location of an upload
|
||||
- Fix crash that occurs randomly while returning to the app after leaving it in the background
|
||||
- Fix crash in Sign up activity on Android version 5.0 and 5.1
|
||||
- Android 13 compatibility changes
|
||||
|
||||
## v4.1.0
|
||||
- Location of pictures uploaded via custom picture selector are now recognized
|
||||
- Improvements to the custom picture selector
|
||||
- Ensure the WLM pictures are associated with the correct templates for each year
|
||||
- Only show pictures uploaded via app in peer review
|
||||
- Improve the variety of images show in peer review
|
||||
- Allow going to current location in location edit dialog while uploading a picture
|
||||
- Switch to using MapLibre instead of Mapbox and thereby disable telemetry sent to Mapbox
|
||||
- Fixed various bugs
|
||||
|
||||
## v4.0.5
|
||||
- Bumped min SDK to 29 to try and solve Google policy issue
|
||||
- Reverted dialog
|
||||
- Note: This encompasses versions 1031, 1032, and 1033, due to the Play Store's requirements to overwrite all the tracks with a post-fix version (otherwise no single track can be published)
|
||||
|
||||
## v4.0.4
|
||||
- Added dialog for Google's location policy
|
||||
|
||||
## v4.0.3
|
||||
- Added "Report" button for Google UGC policy
|
||||
|
||||
|
|
|
|||
1
CREDITS
1
CREDITS
|
|
@ -53,7 +53,6 @@ their contribution to the product.
|
|||
* Butterknife
|
||||
* GSON
|
||||
* Timber
|
||||
* MapBox
|
||||
|
||||
3rd party open source apps from which significant code has been reused:
|
||||
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
||||
|
||||
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
|
||||
Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request! :-)
|
||||
|
||||
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a>
|
||||
|
|
@ -15,7 +15,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
|
|||
|
||||
## Documentation
|
||||
|
||||
We try to have an extensive documentation at our [documentation repository][4]:
|
||||
Our [documentation repository][4] contains extensive documentation for users, contributors, and developers alike:
|
||||
|
||||
* [User Documentation][5]
|
||||
* [Contributor Documentation][6]
|
||||
|
|
@ -29,11 +29,11 @@ Thank you all for your work!
|
|||
|
||||
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/54016427?v=4" width="100px;"/><br /><sub><b>4D17Y4</b></sub>](https://github.com/4D17Y4) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) |
|
||||
|
||||
|
||||
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
||||
|
|
|
|||
115
app/build.gradle
115
app/build.gradle
|
|
@ -5,7 +5,7 @@ apply from: '../gitutils.gradle'
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: "$rootDir/jacoco.gradle"
|
||||
|
||||
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
||||
|
|
@ -16,13 +16,17 @@ if (isRunningOnTravisAndIsNotPRBuild) {
|
|||
|
||||
dependencies {
|
||||
|
||||
implementation project(':wikimedia-data-client')
|
||||
// Utils
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"){
|
||||
force = true //API 19 support
|
||||
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){
|
||||
// Forcing dependency versions using force = true on a first-level dependency has been deprecated.
|
||||
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
|
||||
//force = true //API 19 support
|
||||
}
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
|
||||
implementation 'com.squareup.okio:okio:2.2.2'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
|
||||
|
|
@ -45,10 +49,8 @@ dependencies {
|
|||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
|
||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
||||
|
|
@ -73,30 +75,33 @@ dependencies {
|
|||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||
|
||||
//Mocking
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
testImplementation 'org.mockito:mockito-core:2.25.1'
|
||||
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
|
||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
||||
testImplementation 'androidx.test:core:1.4.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation "androidx.test:runner:1.5.2"
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
testImplementation "androidx.test:rules:1.5.0"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
|
||||
testImplementation "com.jraska.livedata:testing-ktx:1.1.2"
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0"
|
||||
testImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"
|
||||
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
|
||||
testImplementation 'com.facebook.soloader:soloader:0.10.5'
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
|
||||
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
|
||||
testImplementation "commons-io:commons-io:2.6"
|
||||
|
||||
// Android testing
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04'
|
||||
|
|
@ -119,8 +124,7 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||
|
||||
//swipe_layout
|
||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
||||
|
|
@ -131,7 +135,6 @@ dependencies {
|
|||
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
|
||||
kapt "androidx.room:room-compiler:$ROOM_VERSION"
|
||||
// For Kotlin use kapt instead of annotationProcessor
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
|
||||
// Pref
|
||||
|
|
@ -139,10 +142,12 @@ dependencies {
|
|||
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
||||
// Kotlin
|
||||
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
||||
//Android Media
|
||||
implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
|
||||
|
||||
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
||||
|
||||
def work_version = "2.8.0"
|
||||
def work_version = "2.8.1"
|
||||
// Kotlin + coroutines
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation("androidx.work:work-runtime:$work_version")
|
||||
|
|
@ -151,8 +156,21 @@ dependencies {
|
|||
//Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
|
||||
|
||||
implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }
|
||||
|
||||
//OSMDroid
|
||||
implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION")
|
||||
constraints {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
|
||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||
}
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
|
||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task disableAnimations(type: Exec) {
|
||||
|
|
@ -168,17 +186,17 @@ project.gradle.taskGraph.whenReady {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 33
|
||||
|
||||
defaultConfig {
|
||||
//applicationId 'fr.free.nrw.commons'
|
||||
|
||||
versionCode 1029
|
||||
versionName '4.0.3'
|
||||
versionCode 1040
|
||||
versionName '5.0.2'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
targetSdkVersion 33
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
|
|
@ -188,17 +206,23 @@ android {
|
|||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/androidx.*'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
jniLibs {
|
||||
excludes += ['META-INF/androidx.*']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
testOptions {
|
||||
animationsDisabled true
|
||||
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests {
|
||||
returnDefaultValues = true
|
||||
includeAndroidResources = true
|
||||
}
|
||||
|
||||
unitTests.all {
|
||||
jvmArgs '-noverify'
|
||||
|
|
@ -223,13 +247,14 @@ android {
|
|||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
testProguardFile 'test-proguard-rules.txt'
|
||||
signingConfig signingConfigs.debug
|
||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
testCoverageEnabled true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
testProguardFile 'test-proguard-rules.txt'
|
||||
versionNameSuffix "-debug-" + getBranchName()
|
||||
|
|
@ -284,7 +309,6 @@ android {
|
|||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
||||
|
||||
dimension 'tier'
|
||||
}
|
||||
|
||||
|
|
@ -320,20 +344,17 @@ android {
|
|||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
||||
|
||||
dimension 'tier'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
disable 'ExtraTranslation'
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildToolsVersion buildToolsVersion
|
||||
|
|
@ -341,7 +362,11 @@ android {
|
|||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
namespace 'fr.free.nrw.commons'
|
||||
lint {
|
||||
abortOnError false
|
||||
disable 'MissingTranslation', 'ExtraTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
String getTestUserName() {
|
||||
|
|
@ -371,7 +396,3 @@ if (isRunningOnTravisAndIsNotPRBuild) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@
|
|||
-keepattributes Signature
|
||||
# Retain declared checked exceptions for use by a Proxy instance.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# Note: The model package right now seems to include some other classes that
|
||||
# are not used for serialization / deserialization over Gson. Hopefully
|
||||
# that's not a problem since it only prevents R8 from avoiding trimming
|
||||
# of few more classes.
|
||||
-keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; }
|
||||
-keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; }
|
||||
-keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; }
|
||||
-keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; }
|
||||
-keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; }
|
||||
|
||||
# --- /Retrofit ---
|
||||
|
||||
# --- OkHttp + Okio ---
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.room.Room;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import fr.free.nrw.commons.db.AppDatabase;
|
||||
import fr.free.nrw.commons.review.ReviewDao;
|
||||
import fr.free.nrw.commons.review.ReviewEntity;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ReviewDaoTest {
|
||||
|
||||
private ReviewDao reviewDao;
|
||||
private AppDatabase database;
|
||||
|
||||
/**
|
||||
* Set up the application database
|
||||
*/
|
||||
@Before
|
||||
public void createDb() {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
context, AppDatabase.class)
|
||||
.allowMainThreadQueries()
|
||||
.build();
|
||||
reviewDao = database.ReviewDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database
|
||||
*/
|
||||
@After
|
||||
public void closeDb() {
|
||||
database.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test insertion
|
||||
* Also checks isReviewedAlready():
|
||||
* Case 1: When image has been reviewed/skipped by the user
|
||||
*/
|
||||
@Test
|
||||
public void insert() {
|
||||
// Insert data
|
||||
String imageId = "1234";
|
||||
ReviewEntity reviewEntity = new ReviewEntity(imageId);
|
||||
reviewDao.insert(reviewEntity);
|
||||
|
||||
// Check insertion
|
||||
// Covers the case where the image exists in the database
|
||||
// And isReviewedAlready() returns true
|
||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
||||
assertThat(isInserted, equalTo(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test review status of the image
|
||||
* Case 2: When image has not been reviewed/skipped
|
||||
*/
|
||||
@Test
|
||||
public void isReviewedAlready(){
|
||||
String imageId = "5856";
|
||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
||||
assertThat(isInserted, equalTo(false));
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ class UploadTest {
|
|||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
||||
|
||||
onView(withId(R.id.btn_add_description))
|
||||
onView(withId(R.id.btn_add))
|
||||
.perform(click())
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
|
|
|
|||
|
|
@ -1,256 +1,266 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="fr.free.nrw.commons">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
|
||||
|
||||
<queries>
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<!-- Google Maps -->
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
<queries>
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<!-- Google Maps -->
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
|
||||
|
||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
|
||||
<application
|
||||
android:name=".CommonsApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/LightAppTheme"
|
||||
android:largeHeap="true"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:appComponentFactory"
|
||||
android:appComponentFactory="commons"
|
||||
android:requestLegacyExternalStorage = "true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<application
|
||||
android:name=".CommonsApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/LightAppTheme"
|
||||
android:largeHeap="true"
|
||||
android:supportsRtl="true"
|
||||
tools:replace="android:appComponentFactory"
|
||||
android:appComponentFactory="commons"
|
||||
android:requestLegacyExternalStorage = "true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity
|
||||
android:name=".description.DescriptionEditActivity"
|
||||
android:exported="true" />
|
||||
<activity
|
||||
android:name=".nearby.WikidataFeedback"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||
android:process=":acra"
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
<activity
|
||||
android:theme="@style/EditActivityTheme"
|
||||
android:name=".description.DescriptionEditActivity"
|
||||
android:exported="true" />
|
||||
|
||||
<activity
|
||||
android:name=".media.ZoomableActivity"
|
||||
android:label="Zoomable Activity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||
<activity
|
||||
android:name=".edit.EditActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name=".auth.LoginActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||
android:process=":acra"
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<activity
|
||||
android:name=".media.ZoomableActivity"
|
||||
android:label="Zoomable Activity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
<activity android:name=".auth.LoginActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
</activity>
|
||||
<activity android:name=".WelcomeActivity" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
|
||||
<activity
|
||||
android:hardwareAccelerated="false"
|
||||
android:name=".upload.UploadActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</activity>
|
||||
<activity android:name=".WelcomeActivity" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<activity
|
||||
android:hardwareAccelerated="false"
|
||||
android:name=".upload.UploadActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".contributions.MainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="screenSize|keyboard|orientation" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings" />
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<activity
|
||||
android:name=".auth.SignupActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/title_activity_signup" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<activity
|
||||
android:name=".notification.NotificationActivity"
|
||||
android:label="@string/navigation_item_notification" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".contributions.MainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="screenSize|keyboard|orientation" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings" />
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity android:name=".quiz.QuizActivity"
|
||||
android:label="@string/quiz"/>
|
||||
<activity
|
||||
android:name=".auth.SignupActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/title_activity_signup" />
|
||||
|
||||
<activity android:name=".quiz.QuizResultActivity"
|
||||
android:label="@string/result"/>
|
||||
<activity
|
||||
android:name=".notification.NotificationActivity"
|
||||
android:label="@string/navigation_item_notification" />
|
||||
|
||||
<activity
|
||||
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
||||
android:label="@string/title_activity_custom_selector"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity android:name=".quiz.QuizActivity"
|
||||
android:label="@string/quiz"/>
|
||||
|
||||
<activity
|
||||
android:name=".category.CategoryDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity android:name=".quiz.QuizResultActivity"
|
||||
android:label="@string/result"/>
|
||||
|
||||
<activity
|
||||
android:name=".explore.depictions.WikidataItemDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity
|
||||
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
||||
android:label="@string/title_activity_custom_selector"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".explore.SearchActivity"
|
||||
android:label="@string/title_activity_search"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:parentActivityName=".contributions.MainActivity"
|
||||
/>
|
||||
<activity
|
||||
android:name=".category.CategoryDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:label="@string/Profile" />
|
||||
<activity
|
||||
android:name=".explore.depictions.WikidataItemDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".review.ReviewActivity"
|
||||
android:label="@string/title_activity_review" />
|
||||
<activity
|
||||
android:name=".explore.SearchActivity"
|
||||
android:label="@string/title_activity_search"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:parentActivityName=".contributions.MainActivity"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".LocationPicker.LocationPickerActivity"
|
||||
android:label="Location Picker" />
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:label="@string/Profile" />
|
||||
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
<activity
|
||||
android:name=".review.ReviewActivity"
|
||||
android:label="@string/title_activity_review" />
|
||||
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
android:exported="false"
|
||||
android:process=":acra" />
|
||||
<activity
|
||||
android:name=".LocationPicker.LocationPickerActivity"
|
||||
android:label="Location Picker" />
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.ExtendedFileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
android:syncable="false" />
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
android:exported="false"
|
||||
android:process=":acra" />
|
||||
|
||||
<provider
|
||||
android:name=".explore.recentsearches.RecentSearchesContentProvider"
|
||||
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_searches"
|
||||
android:syncable="false" />
|
||||
<provider
|
||||
android:name=".filepicker.ExtendedFileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".recentlanguages.RecentLanguagesContentProvider"
|
||||
android:authorities="${applicationId}.recentlanguages.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_recent_languages"
|
||||
android:syncable="false" />
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
android:syncable="false" />
|
||||
|
||||
<provider
|
||||
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks"
|
||||
android:syncable="false" />
|
||||
<provider
|
||||
android:name=".explore.recentsearches.RecentSearchesContentProvider"
|
||||
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_searches"
|
||||
android:syncable="false" />
|
||||
|
||||
<provider
|
||||
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
<provider
|
||||
android:name=".recentlanguages.RecentLanguagesContentProvider"
|
||||
android:authorities="${applicationId}.recentlanguages.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_recent_languages"
|
||||
android:syncable="false" />
|
||||
|
||||
<provider
|
||||
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
<provider
|
||||
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks"
|
||||
android:syncable="false" />
|
||||
|
||||
<receiver android:name=".widget.PicOfDayAppWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<provider
|
||||
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||
</receiver>
|
||||
<provider
|
||||
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false" />
|
||||
<receiver android:name=".widget.PicOfDayAppWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
</application>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||
</receiver>
|
||||
|
||||
</manifest>
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
64
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
64
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
|
||||
class BaseMarker {
|
||||
private var _position: LatLng = LatLng(0.0, 0.0, 0f)
|
||||
private var _title: String = ""
|
||||
private var _place: Place = Place()
|
||||
private var _icon: Bitmap? = null
|
||||
|
||||
var position: LatLng
|
||||
get() = _position
|
||||
set(value) {
|
||||
_position = value
|
||||
}
|
||||
var title: String
|
||||
get() = _title
|
||||
set(value) {
|
||||
_title = value
|
||||
}
|
||||
|
||||
var place: Place
|
||||
get() = _place
|
||||
set(value) {
|
||||
_place = value
|
||||
}
|
||||
var icon: Bitmap?
|
||||
get() = _icon
|
||||
set(value) {
|
||||
_icon = value
|
||||
}
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
fun fromResource(context: Context, drawableResId: Int) {
|
||||
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
||||
icon = if (drawable is BitmapDrawable) {
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
} else {
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readDouble(),
|
||||
parcel.readDouble(),
|
||||
parcel.readDouble()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeDouble(latitude)
|
||||
parcel.writeDouble(longitude)
|
||||
parcel.writeDouble(zoom)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||
override fun createFromParcel(parcel: Parcel): CameraPosition {
|
||||
return CameraPosition(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CameraPosition?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.wikipedia.AppAdapter;
|
||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.json.GsonMarshaller;
|
||||
import org.wikipedia.json.GsonUnmarshaller;
|
||||
import org.wikipedia.login.LoginResult;
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class CommonsAppAdapter extends AppAdapter {
|
||||
private final int DEFAULT_THUMB_SIZE = 640;
|
||||
private final String COOKIE_STORE_NAME = "cookie_store";
|
||||
|
||||
private final SessionManager sessionManager;
|
||||
private final JsonKvStore preferences;
|
||||
|
||||
CommonsAppAdapter(@NonNull SessionManager sessionManager, @NonNull JsonKvStore preferences) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMediaWikiBaseUrl() {
|
||||
return BuildConfig.COMMONS_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRestbaseUriFormat() {
|
||||
return BuildConfig.COMMONS_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
|
||||
return OkHttpConnectionFactory.getClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDesiredLeadImageDp() {
|
||||
return DEFAULT_THUMB_SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggedIn() {
|
||||
return sessionManager.isUserLoggedIn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserName() {
|
||||
return sessionManager.getUserName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return sessionManager.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAccount(@NonNull LoginResult result) {
|
||||
sessionManager.updateAccount(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SharedPreferenceCookieManager getCookies() {
|
||||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
|
||||
preferences.getString(COOKIE_STORE_NAME, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
|
||||
preferences.putString(COOKIE_STORE_NAME, GsonMarshaller.marshal(cookies));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logErrorsInsteadOfCrashing() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,22 +9,22 @@ import static org.acra.ReportField.STACK_TRACE;
|
|||
import static org.acra.ReportField.USER_COMMENT;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.multidex.BuildConfig;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
||||
import com.mapbox.mapboxsdk.Mapbox;
|
||||
import com.mapbox.mapboxsdk.WellKnownTileServer;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
|
|
@ -36,12 +36,14 @@ import fr.free.nrw.commons.contributions.ContributionDao;
|
|||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
|
||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
||||
import fr.free.nrw.commons.logging.LogUtils;
|
||||
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.FileUtils;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.internal.functions.Functions;
|
||||
|
|
@ -59,8 +61,6 @@ import org.acra.annotation.AcraCore;
|
|||
import org.acra.annotation.AcraDialog;
|
||||
import org.acra.annotation.AcraMailSender;
|
||||
import org.acra.data.StringFormat;
|
||||
import org.wikipedia.AppAdapter;
|
||||
import org.wikipedia.language.AppLanguageLookUpTable;
|
||||
import timber.log.Timber;
|
||||
|
||||
@AcraCore(
|
||||
|
|
@ -85,6 +85,9 @@ import timber.log.Timber;
|
|||
|
||||
public class CommonsApplication extends MultiDexApplication {
|
||||
|
||||
public static final String loginMessageIntentKey = "loginMessage";
|
||||
public static final String loginUsernameIntentKey = "loginUsername";
|
||||
|
||||
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
|
@ -95,6 +98,9 @@ public class CommonsApplication extends MultiDexApplication {
|
|||
@Named("default_preferences")
|
||||
JsonKvStore defaultPrefs;
|
||||
|
||||
@Inject
|
||||
CommonsCookieJar cookieJar;
|
||||
|
||||
@Inject
|
||||
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
|
||||
|
||||
|
|
@ -137,10 +143,15 @@ public class CommonsApplication extends MultiDexApplication {
|
|||
ContributionDao contributionDao;
|
||||
|
||||
/**
|
||||
* In-memory list of contributions whose uploads have been paused by the user
|
||||
* In-memory list of contributions whose uploads have been paused by the user
|
||||
*/
|
||||
public static Map<String, Boolean> pauseUploads = new HashMap<>();
|
||||
|
||||
/**
|
||||
* In-memory list of uploads that have been cancelled by the user
|
||||
*/
|
||||
public static HashSet<String> cancelledUploads = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
|
|
@ -150,15 +161,12 @@ public class CommonsApplication extends MultiDexApplication {
|
|||
|
||||
INSTANCE = this;
|
||||
ACRA.init(this);
|
||||
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token), WellKnownTileServer.Mapbox);
|
||||
|
||||
ApplicationlessInjection
|
||||
.getInstance(this)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
|
||||
|
||||
initTimber();
|
||||
|
||||
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
||||
|
|
@ -286,6 +294,7 @@ public class CommonsApplication extends MultiDexApplication {
|
|||
}
|
||||
|
||||
sessionManager.logout()
|
||||
.andThen(Completable.fromAction(() -> cookieJar.clear()))
|
||||
.andThen(Completable.fromAction(() -> {
|
||||
Timber.d("All accounts have been removed");
|
||||
clearImageCache();
|
||||
|
|
@ -337,4 +346,96 @@ public class CommonsApplication extends MultiDexApplication {
|
|||
|
||||
void onLogoutComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
|
||||
* with relevant intent parameters. It does not perform the actual logout operation.
|
||||
*/
|
||||
public static class BaseLogoutListener implements CommonsApplication.LogoutListener {
|
||||
|
||||
Context ctx;
|
||||
String loginMessage, userName;
|
||||
|
||||
/**
|
||||
* Constructor for BaseLogoutListener.
|
||||
*
|
||||
* @param ctx Application context
|
||||
*/
|
||||
public BaseLogoutListener(final Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for BaseLogoutListener
|
||||
*
|
||||
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||
* @param loginMessage Message to be displayed on the login page
|
||||
* @param loginUsername Username to be pre-filled on the login page
|
||||
*/
|
||||
public BaseLogoutListener(final Context ctx, final String loginMessage,
|
||||
final String loginUsername) {
|
||||
this.ctx = ctx;
|
||||
this.loginMessage = loginMessage;
|
||||
this.userName = loginUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLogoutComplete() {
|
||||
Timber.d("Logout complete callback received.");
|
||||
final Intent loginIntent = new Intent(ctx, LoginActivity.class);
|
||||
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
if (loginMessage != null) {
|
||||
loginIntent.putExtra(loginMessageIntentKey, loginMessage);
|
||||
}
|
||||
if (userName != null) {
|
||||
loginIntent.putExtra(loginUsernameIntentKey, userName);
|
||||
}
|
||||
|
||||
ctx.startActivity(loginIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
|
||||
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
|
||||
*/
|
||||
public static class ActivityLogoutListener extends BaseLogoutListener {
|
||||
|
||||
Activity activity;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for ActivityLogoutListener.
|
||||
*
|
||||
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||
*/
|
||||
public ActivityLogoutListener(final Activity activity, final Context ctx) {
|
||||
super(ctx);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
|
||||
*
|
||||
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||
* @param loginMessage Message to be displayed on the login page after logout.
|
||||
* @param loginUsername Username to be pre-filled on the login page after logout.
|
||||
*/
|
||||
public ActivityLogoutListener(final Activity activity, final Context ctx,
|
||||
final String loginMessage, final String loginUsername) {
|
||||
super(activity, loginMessage, loginUsername);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLogoutComplete() {
|
||||
super.onLogoutComplete();
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ package fr.free.nrw.commons.LocationPicker;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import fr.free.nrw.commons.CameraPosition;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
||||
/**
|
||||
* Helper class for starting the activity
|
||||
|
|
@ -52,6 +53,17 @@ public final class LocationPicker {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and puts media in intent
|
||||
* @param media Media
|
||||
* @return LocationPicker.IntentBuilder
|
||||
*/
|
||||
public LocationPicker.IntentBuilder media(
|
||||
final Media media) {
|
||||
intent.putExtra(LocationPickerConstants.MEDIA, media);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the activity
|
||||
* @param activity Activity
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
|
||||
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
|
||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
|
||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
|
||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
|
||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
|
||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
|
||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
|
||||
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.location.Location;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
|
|
@ -28,52 +30,56 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.mapbox.geojson.Point;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition.Builder;
|
||||
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
|
||||
import com.mapbox.mapboxsdk.geometry.LatLng;
|
||||
import com.mapbox.mapboxsdk.location.LocationComponent;
|
||||
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
|
||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback;
|
||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineResult;
|
||||
import com.mapbox.mapboxsdk.location.modes.CameraMode;
|
||||
import com.mapbox.mapboxsdk.location.modes.RenderMode;
|
||||
import com.mapbox.mapboxsdk.location.permissions.PermissionsManager;
|
||||
import com.mapbox.mapboxsdk.maps.MapView;
|
||||
import com.mapbox.mapboxsdk.maps.MapboxMap;
|
||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener;
|
||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener;
|
||||
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
|
||||
import com.mapbox.mapboxsdk.maps.Style;
|
||||
import com.mapbox.mapboxsdk.maps.UiSettings;
|
||||
import com.mapbox.mapboxsdk.style.layers.Layer;
|
||||
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
|
||||
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
|
||||
import fr.free.nrw.commons.MapStyle;
|
||||
import fr.free.nrw.commons.CameraPosition;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
|
||||
import fr.free.nrw.commons.filepicker.Constants;
|
||||
import fr.free.nrw.commons.kvstore.BasicKvStore;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.util.constants.GeoConstants;
|
||||
import org.osmdroid.views.CustomZoomButtonsController;
|
||||
import org.osmdroid.views.overlay.Marker;
|
||||
import org.osmdroid.views.overlay.Overlay;
|
||||
import org.osmdroid.views.overlay.ScaleDiskOverlay;
|
||||
import org.osmdroid.views.overlay.TilesOverlay;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Helps to pick location and return the result with an intent
|
||||
*/
|
||||
public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback,
|
||||
OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> {
|
||||
|
||||
public class LocationPickerActivity extends BaseActivity implements
|
||||
LocationPermissionCallback {
|
||||
/**
|
||||
* DROPPED_MARKER_LAYER_ID : id for layer
|
||||
* coordinateEditHelper: helps to edit coordinates
|
||||
*/
|
||||
private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID";
|
||||
@Inject
|
||||
CoordinateEditHelper coordinateEditHelper;
|
||||
/**
|
||||
* media : Media object
|
||||
*/
|
||||
private Media media;
|
||||
/**
|
||||
* cameraPosition : position of picker
|
||||
*/
|
||||
|
|
@ -83,13 +89,9 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
*/
|
||||
private ImageView markerImage;
|
||||
/**
|
||||
* mapboxMap : map
|
||||
* mapView : OSM Map
|
||||
*/
|
||||
private MapboxMap mapboxMap;
|
||||
/**
|
||||
* mapView : view of the map
|
||||
*/
|
||||
private MapView mapView;
|
||||
private org.osmdroid.views.MapView mapView;
|
||||
/**
|
||||
* tvAttribution : credit
|
||||
*/
|
||||
|
|
@ -98,14 +100,14 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
* activity : activity key
|
||||
*/
|
||||
private String activity;
|
||||
/**
|
||||
* location : location
|
||||
*/
|
||||
private Location location;
|
||||
/**
|
||||
* modifyLocationButton : button for start editing location
|
||||
*/
|
||||
Button modifyLocationButton;
|
||||
/**
|
||||
* removeLocationButton : button to remove location metadata
|
||||
*/
|
||||
Button removeLocationButton;
|
||||
/**
|
||||
* showInMapButton : button for showing in map
|
||||
*/
|
||||
|
|
@ -118,10 +120,6 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
* fabCenterOnLocation: button for center on location;
|
||||
*/
|
||||
FloatingActionButton fabCenterOnLocation;
|
||||
/**
|
||||
* droppedMarkerLayer : Layer for static screen
|
||||
*/
|
||||
private Layer droppedMarkerLayer;
|
||||
/**
|
||||
* shadow : imageview of shadow
|
||||
*/
|
||||
|
|
@ -141,19 +139,38 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
@Named("default_preferences")
|
||||
public
|
||||
JsonKvStore applicationKvStore;
|
||||
BasicKvStore store;
|
||||
/**
|
||||
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
|
||||
*/
|
||||
@Inject
|
||||
SystemThemeUtils systemThemeUtils;
|
||||
private boolean isDarkTheme;
|
||||
private boolean moveToCurrentLocation;
|
||||
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
LocationPermissionsHelper locationPermissionsHelper;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
private static final String CAMERA_POS = "cameraPosition";
|
||||
private static final String ACTIVITY = "activity";
|
||||
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
||||
moveToCurrentLocation = false;
|
||||
store = new BasicKvStore(this, "LocationPermissions");
|
||||
|
||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
|
|
@ -166,12 +183,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
cameraPosition = getIntent()
|
||||
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
||||
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
|
||||
media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA);
|
||||
}else{
|
||||
cameraPosition = savedInstanceState.getParcelable(CAMERA_POS);
|
||||
activity = savedInstanceState.getString(ACTIVITY);
|
||||
media = savedInstanceState.getParcelable("sMedia");
|
||||
}
|
||||
|
||||
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
|
||||
.get(LocationPickerViewModel.class);
|
||||
viewModel.getResult().observe(this, this);
|
||||
|
||||
bindViews();
|
||||
addBackButtonListener();
|
||||
addPlaceSelectedButton();
|
||||
|
|
@ -179,18 +196,57 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
getToolbarUI();
|
||||
addCenterOnGPSButton();
|
||||
|
||||
org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(),
|
||||
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()));
|
||||
|
||||
mapView.setTileSource(TileSourceFactory.WIKIMEDIA);
|
||||
mapView.setTilesScaledToDpi(true);
|
||||
mapView.setMultiTouchControls(true);
|
||||
|
||||
org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put(
|
||||
"Referer", "http://maps.wikimedia.org/"
|
||||
);
|
||||
mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
|
||||
mapView.getController().setZoom(ZOOM_LEVEL);
|
||||
mapView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
if (markerImage.getTranslationY() == 0) {
|
||||
markerImage.animate().translationY(-75)
|
||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||
}
|
||||
} else if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
markerImage.animate().translationY(0)
|
||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if ("UploadActivity".equals(activity)) {
|
||||
placeSelectedButton.setVisibility(View.GONE);
|
||||
modifyLocationButton.setVisibility(View.VISIBLE);
|
||||
removeLocationButton.setVisibility(View.VISIBLE);
|
||||
showInMapButton.setVisibility(View.VISIBLE);
|
||||
largeToolbarText.setText(getResources().getString(R.string.image_location));
|
||||
smallToolbarText.setText(getResources().
|
||||
getString(R.string.check_whether_location_is_correct));
|
||||
fabCenterOnLocation.setVisibility(View.GONE);
|
||||
markerImage.setVisibility(View.GONE);
|
||||
shadow.setVisibility(View.GONE);
|
||||
assert cameraPosition != null;
|
||||
showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(),
|
||||
cameraPosition.getLongitude()));
|
||||
}
|
||||
setupMapView();
|
||||
|
||||
if("UploadActivity".equals(activity)){
|
||||
if(mapView != null && mapView.getController() != null && cameraPosition != null){
|
||||
GeoPoint cameraGeoPoint = new GeoPoint(cameraPosition.getLatitude(),
|
||||
cameraPosition.getLongitude());
|
||||
|
||||
mapView.onCreate(savedInstanceState);
|
||||
mapView.getMapAsync(this);
|
||||
mapView.getController().setCenter(cameraGeoPoint);
|
||||
mapView.getController().animateTo(cameraGeoPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -201,12 +257,26 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* For setting up Dark Theme
|
||||
*/
|
||||
private void darkThemeSetup() {
|
||||
if (isDarkTheme) {
|
||||
shadow.setColorFilter(Color.argb(255, 255, 255, 255));
|
||||
mapView.getOverlayManager().getTilesOverlay()
|
||||
.setColorFilter(TilesOverlay.INVERT_COLORS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking back button destroy locationPickerActivity
|
||||
*/
|
||||
private void addBackButtonListener() {
|
||||
final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button);
|
||||
backButton.setOnClickListener(view -> finish());
|
||||
backButton.setOnClickListener(v -> {
|
||||
finish();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -217,21 +287,12 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
markerImage = findViewById(R.id.location_picker_image_view_marker);
|
||||
tvAttribution = findViewById(R.id.tv_attribution);
|
||||
modifyLocationButton = findViewById(R.id.modify_location);
|
||||
removeLocationButton = findViewById(R.id.remove_location);
|
||||
showInMapButton = findViewById(R.id.show_in_map);
|
||||
showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase());
|
||||
shadow = findViewById(R.id.location_picker_image_view_shadow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the listeners
|
||||
*/
|
||||
private void bindListeners() {
|
||||
mapboxMap.addOnCameraMoveStartedListener(
|
||||
this);
|
||||
mapboxMap.addOnCameraIdleListener(
|
||||
this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets toolbar color
|
||||
*/
|
||||
|
|
@ -242,49 +303,13 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes action when map is ready to show
|
||||
* @param mapboxMap map
|
||||
*/
|
||||
@Override
|
||||
public void onMapReady(final MapboxMap mapboxMap) {
|
||||
this.mapboxMap = mapboxMap;
|
||||
mapboxMap.setStyle(isDarkTheme ? MapStyle.DARK : MapStyle.STREETS, this::onStyleLoaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes dropped marker and layer
|
||||
* Handles camera position based on options
|
||||
* Enables location components
|
||||
*
|
||||
* @param style style
|
||||
*/
|
||||
private void onStyleLoaded(final Style style) {
|
||||
if (modifyLocationButton.getVisibility() == View.VISIBLE) {
|
||||
initDroppedMarker(style);
|
||||
adjustCameraBasedOnOptions();
|
||||
enableLocationComponent(style);
|
||||
if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) {
|
||||
final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id");
|
||||
if (source != null) {
|
||||
source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(),
|
||||
cameraPosition.target.getLatitude()));
|
||||
}
|
||||
droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID);
|
||||
if (droppedMarkerLayer != null) {
|
||||
droppedMarkerLayer.setProperties(visibility(VISIBLE));
|
||||
markerImage.setVisibility(View.GONE);
|
||||
shadow.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
adjustCameraBasedOnOptions();
|
||||
enableLocationComponent(style);
|
||||
bindListeners();
|
||||
}
|
||||
|
||||
private void setupMapView() {
|
||||
adjustCameraBasedOnOptions();
|
||||
modifyLocationButton.setOnClickListener(v -> onClickModifyLocation());
|
||||
removeLocationButton.setOnClickListener(v -> onClickRemoveLocation());
|
||||
showInMapButton.setOnClickListener(v -> showInMap());
|
||||
darkThemeSetup();
|
||||
requestLocationPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -293,133 +318,70 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
private void onClickModifyLocation() {
|
||||
placeSelectedButton.setVisibility(View.VISIBLE);
|
||||
modifyLocationButton.setVisibility(View.GONE);
|
||||
removeLocationButton.setVisibility(View.GONE);
|
||||
showInMapButton.setVisibility(View.GONE);
|
||||
droppedMarkerLayer.setProperties(visibility(NONE));
|
||||
markerImage.setVisibility(View.VISIBLE);
|
||||
shadow.setVisibility(View.VISIBLE);
|
||||
largeToolbarText.setText(getResources().getString(R.string.choose_a_location));
|
||||
smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust));
|
||||
bindListeners();
|
||||
fabCenterOnLocation.setVisibility(View.VISIBLE);
|
||||
removeSelectedLocationMarker();
|
||||
if (cameraPosition != null && mapView != null) {
|
||||
if (mapView.getController() != null) {
|
||||
mapView.getController().animateTo(new GeoPoint(cameraPosition.getLatitude(),
|
||||
cameraPosition.getLongitude()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles onclick event of removeLocationButton
|
||||
*/
|
||||
private void onClickRemoveLocation() {
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.remove_location_warning_title),
|
||||
getString(R.string.remove_location_warning_desc),
|
||||
getString(R.string.continue_message),
|
||||
getString(R.string.cancel), () -> removeLocationFromImage(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to remove the location from the picture
|
||||
*/
|
||||
private void removeLocationFromImage() {
|
||||
if (media != null) {
|
||||
compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
|
||||
, media, "0.0", "0.0", "0.0f")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s -> {
|
||||
Timber.d("Coordinates are removed from the image");
|
||||
}));
|
||||
}
|
||||
final Intent returningIntent = new Intent();
|
||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the location in map app
|
||||
*/
|
||||
public void showInMap(){
|
||||
public void showInMap() {
|
||||
Utils.handleGeoCoordinates(this,
|
||||
new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(),
|
||||
cameraPosition.target.getLongitude(), 0.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Dropped Marker and layer without showing
|
||||
* @param loadedMapStyle style
|
||||
*/
|
||||
private void initDroppedMarker(@NonNull final Style loadedMapStyle) {
|
||||
// Add the marker image to map
|
||||
loadedMapStyle.addImage("dropped-icon-image", BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.map_default_map_marker));
|
||||
loadedMapStyle.addSource(new GeoJsonSource("dropped-marker-source-id"));
|
||||
loadedMapStyle.addLayer(new SymbolLayer(DROPPED_MARKER_LAYER_ID,
|
||||
"dropped-marker-source-id").withProperties(
|
||||
iconImage("dropped-icon-image"),
|
||||
visibility(NONE),
|
||||
iconAllowOverlap(true),
|
||||
iconIgnorePlacement(true)
|
||||
));
|
||||
new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(),
|
||||
mapView.getMapCenter().getLongitude(), 0.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* move the location to the current media coordinates
|
||||
*/
|
||||
private void adjustCameraBasedOnOptions() {
|
||||
mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables location components
|
||||
* @param loadedMapStyle Style
|
||||
*/
|
||||
@SuppressWarnings( {"MissingPermission"})
|
||||
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
|
||||
final UiSettings uiSettings = mapboxMap.getUiSettings();
|
||||
uiSettings.setAttributionEnabled(false);
|
||||
|
||||
// Check if permissions are enabled and if not request
|
||||
if (PermissionsManager.areLocationPermissionsGranted(this)) {
|
||||
|
||||
// Get an instance of the component
|
||||
final LocationComponent locationComponent = mapboxMap.getLocationComponent();
|
||||
|
||||
// Activate with options
|
||||
locationComponent.activateLocationComponent(
|
||||
LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
|
||||
|
||||
// Enable to make component visible
|
||||
locationComponent.setLocationComponentEnabled(true);
|
||||
|
||||
// Set the component's camera mode
|
||||
locationComponent.setCameraMode(CameraMode.NONE);
|
||||
|
||||
// Set the component's render mode
|
||||
locationComponent.setRenderMode(RenderMode.NORMAL);
|
||||
|
||||
// Get the component's location engine to receive user's last location
|
||||
locationComponent.getLocationEngine().getLastLocation(
|
||||
new LocationEngineCallback<LocationEngineResult>() {
|
||||
@Override
|
||||
public void onSuccess(LocationEngineResult result) {
|
||||
location = result.getLastLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Exception exception) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (cameraPosition != null) {
|
||||
mapView.getController().setCenter(new GeoPoint(cameraPosition.getLatitude(),
|
||||
cameraPosition.getLongitude()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on camera moving
|
||||
* @param reason int
|
||||
*/
|
||||
@Override
|
||||
public void onCameraMoveStarted(final int reason) {
|
||||
Timber.v("Map camera has begun moving.");
|
||||
if (markerImage.getTranslationY() == 0) {
|
||||
markerImage.animate().translationY(-75)
|
||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on camera idle
|
||||
*/
|
||||
@Override
|
||||
public void onCameraIdle() {
|
||||
Timber.v("Map camera is now idling.");
|
||||
markerImage.animate().translationY(0)
|
||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes action on camera position
|
||||
* @param position position of picker
|
||||
*/
|
||||
@Override
|
||||
public void onChanged(@Nullable CameraPosition position) {
|
||||
if (position == null) {
|
||||
position = new Builder()
|
||||
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
|
||||
mapboxMap.getCameraPosition().target.getLongitude()))
|
||||
.zoom(16).build();
|
||||
}
|
||||
cameraPosition = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the preferable location
|
||||
*/
|
||||
|
|
@ -434,35 +396,127 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
void placeSelected() {
|
||||
if (activity.equals("NoLocationUploadActivity")) {
|
||||
applicationKvStore.putString(LAST_LOCATION,
|
||||
mapboxMap.getCameraPosition().target.getLatitude()
|
||||
mapView.getMapCenter().getLatitude()
|
||||
+ ","
|
||||
+ mapboxMap.getCameraPosition().target.getLongitude());
|
||||
applicationKvStore.putString(LAST_ZOOM, mapboxMap.getCameraPosition().zoom + "");
|
||||
+ mapView.getMapCenter().getLongitude());
|
||||
applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + "");
|
||||
}
|
||||
final Intent returningIntent = new Intent();
|
||||
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
||||
mapboxMap.getCameraPosition());
|
||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
||||
|
||||
if (media == null) {
|
||||
final Intent returningIntent = new Intent();
|
||||
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
||||
new CameraPosition(mapView.getMapCenter().getLatitude(),
|
||||
mapView.getMapCenter().getLongitude(), 14.0));
|
||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
||||
} else {
|
||||
updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()),
|
||||
String.valueOf(mapView.getMapCenter().getLongitude()),
|
||||
String.valueOf(0.0f));
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetched coordinates are replaced with existing coordinates by a POST API call.
|
||||
* @param Latitude to be added
|
||||
* @param Longitude to be added
|
||||
* @param Accuracy to be added
|
||||
*/
|
||||
public void updateCoordinates(final String Latitude, final String Longitude,
|
||||
final String Accuracy) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
compositeDisposable.add(
|
||||
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
|
||||
Latitude, Longitude, Accuracy)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s -> {
|
||||
Timber.d("Coordinates are added.");
|
||||
}));
|
||||
} catch (Exception e) {
|
||||
if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) {
|
||||
final String username = sessionManager.getUserName();
|
||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
);
|
||||
|
||||
CommonsApplication.getInstance().clearApplicationData(
|
||||
this, logoutListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Center the camera on the last saved location
|
||||
*/
|
||||
private void addCenterOnGPSButton(){
|
||||
private void addCenterOnGPSButton() {
|
||||
fabCenterOnLocation = findViewById(R.id.center_on_gps);
|
||||
fabCenterOnLocation.setOnClickListener(view -> getCenter());
|
||||
fabCenterOnLocation.setOnClickListener(view -> {
|
||||
moveToCurrentLocation = true;
|
||||
requestLocationPermissions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate map to move to desired Latitude and Longitude
|
||||
* Adds selected location marker on the map
|
||||
*/
|
||||
void getCenter() {
|
||||
mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(),location.getLongitude()),15.0));
|
||||
private void showSelectedLocationMarker(GeoPoint point) {
|
||||
Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker);
|
||||
Marker marker = new Marker(mapView);
|
||||
marker.setPosition(point);
|
||||
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
|
||||
marker.setIcon(icon);
|
||||
marker.setInfoWindow(null);
|
||||
mapView.getOverlays().add(marker);
|
||||
mapView.invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes selected location marker from the map
|
||||
*/
|
||||
private void removeSelectedLocationMarker() {
|
||||
List<Overlay> overlays = mapView.getOverlays();
|
||||
for (int i = 0; i < overlays.size(); i++) {
|
||||
if (overlays.get(i) instanceof Marker) {
|
||||
Marker item = (Marker) overlays.get(i);
|
||||
if (cameraPosition.getLatitude() == item.getPosition().getLatitude()
|
||||
&& cameraPosition.getLongitude() == item.getPosition().getLongitude()) {
|
||||
mapView.getOverlays().remove(i);
|
||||
mapView.invalidate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Center the map at user's current location
|
||||
*/
|
||||
private void requestLocationPermissions() {
|
||||
locationPermissionsHelper = new LocationPermissionsHelper(
|
||||
this, locationManager, this);
|
||||
locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title,
|
||||
R.string.upload_map_location_access);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mapView.onStart();
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
if (requestCode == Constants.RequestCodes.LOCATION
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
onLocationPermissionGranted();
|
||||
} else {
|
||||
onLocationPermissionDenied(getString(R.string.upload_map_location_access));
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -478,26 +532,102 @@ public class LocationPickerActivity extends BaseActivity implements OnMapReadyCa
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
mapView.onStop();
|
||||
public void onLocationPermissionDenied(String toastMessage) {
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||
permission.ACCESS_FINE_LOCATION)) {
|
||||
if (!locationPermissionsHelper.checkLocationPermission(this)) {
|
||||
if (store.getBoolean("isPermissionDenied", false)) {
|
||||
// means user has denied location permission twice or checked the "Don't show again"
|
||||
locationPermissionsHelper.showAppSettingsDialog(this,
|
||||
R.string.upload_map_location_access);
|
||||
} else {
|
||||
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
store.putBoolean("isPermissionDenied", true);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final @NotNull Bundle outState) {
|
||||
public void onLocationPermissionGranted() {
|
||||
if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) {
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
locationManager.requestLocationUpdatesFromProvider(
|
||||
LocationManager.NETWORK_PROVIDER);
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
getLocation();
|
||||
} else {
|
||||
getLocation();
|
||||
locationPermissionsHelper.showLocationOffDialog(this,
|
||||
R.string.ask_to_turn_location_on_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets new location if locations services are on, else gets last location
|
||||
*/
|
||||
private void getLocation() {
|
||||
fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
|
||||
if (currLocation != null) {
|
||||
GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(),
|
||||
currLocation.getLongitude());
|
||||
addLocationMarker(currLocationGeopoint);
|
||||
mapView.getController().setCenter(currLocationGeopoint);
|
||||
mapView.getController().animateTo(currLocationGeopoint);
|
||||
markerImage.setTranslationY(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void addLocationMarker(GeoPoint geoPoint) {
|
||||
if (moveToCurrentLocation) {
|
||||
mapView.getOverlays().clear();
|
||||
}
|
||||
ScaleDiskOverlay diskOverlay =
|
||||
new ScaleDiskOverlay(this,
|
||||
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
|
||||
Paint circlePaint = new Paint();
|
||||
circlePaint.setColor(Color.rgb(128, 128, 128));
|
||||
circlePaint.setStyle(Paint.Style.STROKE);
|
||||
circlePaint.setStrokeWidth(2f);
|
||||
diskOverlay.setCirclePaint2(circlePaint);
|
||||
Paint diskPaint = new Paint();
|
||||
diskPaint.setColor(Color.argb(40, 128, 128, 128));
|
||||
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
diskOverlay.setCirclePaint1(diskPaint);
|
||||
diskOverlay.setDisplaySizeMin(900);
|
||||
diskOverlay.setDisplaySizeMax(1700);
|
||||
mapView.getOverlays().add(diskOverlay);
|
||||
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
|
||||
mapView);
|
||||
startMarker.setPosition(geoPoint);
|
||||
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
|
||||
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
|
||||
startMarker.setIcon(
|
||||
ContextCompat.getDrawable(this, R.drawable.current_location_marker));
|
||||
startMarker.setTitle("Your Location");
|
||||
startMarker.setTextLabelFontSize(24);
|
||||
mapView.getOverlays().add(startMarker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the state of the activity
|
||||
* @param outState Bundle
|
||||
*/
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
mapView.onSaveInstanceState(outState);
|
||||
}
|
||||
if(cameraPosition!=null){
|
||||
outState.putParcelable(CAMERA_POS, cameraPosition);
|
||||
}
|
||||
if(activity!=null){
|
||||
outState.putString(ACTIVITY, activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
mapView.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
mapView.onLowMemory();
|
||||
if(media!=null){
|
||||
outState.putParcelable("sMedia", media);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
import com.mapbox.mapboxsdk.maps.Style;
|
||||
|
||||
/**
|
||||
* Constants need for location picking
|
||||
*/
|
||||
|
|
@ -13,6 +11,9 @@ public final class LocationPickerConstants {
|
|||
public static final String MAP_CAMERA_POSITION
|
||||
= "location.picker.cameraPosition";
|
||||
|
||||
public static final String MEDIA
|
||||
= "location.picker.media";
|
||||
|
||||
|
||||
private LocationPickerConstants() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import android.app.Application;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import fr.free.nrw.commons.CameraPosition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public abstract class MapController {
|
|||
public class NearbyPlacesInfo {
|
||||
public List<Place> placeList; // List of nearby places
|
||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||
public LatLng curLatLng; // Current location when this places are populated
|
||||
public LatLng currentLatLng; // Current location when this places are populated
|
||||
public LatLng searchLatLng; // Search location for finding this places
|
||||
public List<Media> mediaList; // Search location for finding this places
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ public abstract class MapController {
|
|||
public class ExplorePlacesInfo {
|
||||
public List<Place> explorePlaceList; // List of nearby places
|
||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||
public LatLng curLatLng; // Current location when this places are populated
|
||||
public LatLng currentLatLng; // Current location when this places are populated
|
||||
public LatLng searchLatLng; // Search location for finding this places
|
||||
public List<Media> mediaList; // Search location for finding this places
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import com.mapbox.mapboxsdk.maps.Style;
|
||||
|
||||
/**
|
||||
* Constants for various map styles
|
||||
*/
|
||||
public final class MapStyle {
|
||||
public static final String DARK = Style.getPredefinedStyle("Dark");
|
||||
public static final String OUTDOORS = Style.getPredefinedStyle("Outdoors");
|
||||
public static final String STREETS = Style.getPredefinedStyle("Streets");
|
||||
}
|
||||
|
|
@ -2,9 +2,8 @@ package fr.free.nrw.commons
|
|||
|
||||
import android.os.Parcelable
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage
|
||||
import org.wikipedia.page.PageTitle
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien
|
|||
return Single.ambArray(
|
||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
mediaClient.getMedia(media.filename)
|
||||
mediaClient.getMediaSuppressingErrors(media.filename)
|
||||
.onErrorResumeNext { Single.never() }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -15,28 +15,26 @@ import okhttp3.Response;
|
|||
import okhttp3.ResponseBody;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
||||
import org.wikipedia.dataclient.okhttp.HttpStatusException;
|
||||
import timber.log.Timber;
|
||||
|
||||
public final class OkHttpConnectionFactory {
|
||||
private static final String CACHE_DIR_NAME = "okhttp-cache";
|
||||
private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
|
||||
@NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(),
|
||||
CACHE_DIR_NAME), NET_CACHE_SIZE);
|
||||
|
||||
@NonNull
|
||||
private static final OkHttpClient CLIENT = createClient();
|
||||
public static OkHttpClient CLIENT;
|
||||
|
||||
@NonNull public static OkHttpClient getClient() {
|
||||
@NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) {
|
||||
if (CLIENT == null) {
|
||||
CLIENT = createClient(cookieJar);
|
||||
}
|
||||
return CLIENT;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static OkHttpClient createClient() {
|
||||
private static OkHttpClient createClient(final CommonsCookieJar cookieJar) {
|
||||
return new OkHttpClient.Builder()
|
||||
.cookieJar(SharedPreferenceCookieManager.getInstance())
|
||||
.cache(NET_CACHE)
|
||||
.cookieJar(cookieJar)
|
||||
.cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null)
|
||||
.connectTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
|
|
@ -69,6 +67,8 @@ public final class OkHttpConnectionFactory {
|
|||
}
|
||||
|
||||
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
||||
private static final String SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log";
|
||||
public static final String SUPPRESS_ERROR_LOG_HEADER = SUPPRESS_ERROR_LOG+": true";
|
||||
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
|
||||
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
|
||||
|
||||
|
|
@ -77,7 +77,16 @@ public final class OkHttpConnectionFactory {
|
|||
@Override
|
||||
@NonNull
|
||||
public Response intercept(@NonNull final Chain chain) throws IOException {
|
||||
final Response rsp = chain.proceed(chain.request());
|
||||
final Request rq = chain.request();
|
||||
|
||||
// If the request contains our special "suppress errors" header, make note of it
|
||||
// but don't pass that on to the server.
|
||||
final boolean suppressErrors = rq.headers().names().contains(SUPPRESS_ERROR_LOG);
|
||||
final Request request = rq.newBuilder()
|
||||
.removeHeader(SUPPRESS_ERROR_LOG)
|
||||
.build();
|
||||
|
||||
final Response rsp = chain.proceed(request);
|
||||
|
||||
// Do not intercept certain requests and let the caller handle the errors
|
||||
if(isExcludedUrl(chain.request())) {
|
||||
|
|
@ -91,7 +100,12 @@ public final class OkHttpConnectionFactory {
|
|||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Timber.e(e);
|
||||
// Log the error as debug (and therefore, "expected") or at error level
|
||||
if (suppressErrors) {
|
||||
Timber.d(e, "Suppressed (known / expected) error");
|
||||
} else {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
return rsp;
|
||||
}
|
||||
|
|
@ -111,4 +125,30 @@ public final class OkHttpConnectionFactory {
|
|||
|
||||
private OkHttpConnectionFactory() {
|
||||
}
|
||||
|
||||
public static class HttpStatusException extends IOException {
|
||||
private final int code;
|
||||
private final String url;
|
||||
public HttpStatusException(@NonNull Response rsp) {
|
||||
this.code = rsp.code();
|
||||
this.url = rsp.request().url().uri().toString();
|
||||
try {
|
||||
if (rsp.body() != null && rsp.body().contentType() != null
|
||||
&& rsp.body().contentType().toString().contains("json")) {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log?
|
||||
}
|
||||
}
|
||||
|
||||
public int code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String str = "Code: " + code + ", URL: " + url;
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,16 @@ import android.text.SpannableString;
|
|||
import android.text.style.UnderlineSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.page.PageTitle;
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
|
@ -31,9 +29,6 @@ import fr.free.nrw.commons.settings.Prefs;
|
|||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static fr.free.nrw.commons.campaigns.CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE;
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static PageTitle getPageTitle(@NonNull String title) {
|
||||
|
|
@ -137,12 +132,6 @@ public class Utils {
|
|||
*/
|
||||
public static void handleWebUrl(Context context, Uri url) {
|
||||
Timber.d("Launching web url %s", url.toString());
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
|
||||
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
|
||||
toast.show();
|
||||
return;
|
||||
}
|
||||
|
||||
final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
|
||||
|
||||
/**
|
||||
* Response of the Thanks API.
|
||||
* Context:
|
||||
* The Commons Android app lets you thank other contributors who have uploaded a great picture.
|
||||
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||
*/
|
||||
class MwThankPostResponse : MwResponse() {
|
||||
var result: Result? = null
|
||||
|
||||
inner class Result {
|
||||
var success: Int? = null
|
||||
var recipient: String? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This class acts as a Client to facilitate wiki page editing
|
||||
|
|
@ -25,10 +27,48 @@ class PageEditClient(
|
|||
*/
|
||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking)
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(false)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new page with the given title, text, and summary.
|
||||
*
|
||||
* @param pageTitle The title of the page to be created.
|
||||
* @param text The content of the page in wikitext format.
|
||||
* @param summary The edit summary for the page creation.
|
||||
* @return An observable that emits true if the page creation succeeded, false otherwise.
|
||||
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
|
||||
*/
|
||||
fun postCreate(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postCreate(
|
||||
pageTitle,
|
||||
summary,
|
||||
text,
|
||||
"text/x-wiki",
|
||||
"wikitext",
|
||||
true,
|
||||
true,
|
||||
csrfTokenClient.getTokenBlocking()
|
||||
).map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,10 +81,14 @@ class PageEditClient(
|
|||
*/
|
||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking)
|
||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(false)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +101,40 @@ class PageEditClient(
|
|||
*/
|
||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking)
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(false)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a new section to the wiki page
|
||||
* @param pageTitle Title of the page to edit
|
||||
* @param sectionTitle Title of the new section that needs to be created
|
||||
* @param sectionText The page content that is to be added to the section
|
||||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun createNewSection(pageTitle: String, sectionTitle: String, sectionText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set new labels to Wikibase server of commons
|
||||
* @param summary Edit summary
|
||||
|
|
@ -76,9 +147,14 @@ class PageEditClient(
|
|||
language: String, value: String) : Observable<Int>{
|
||||
return try {
|
||||
pageEditInterface.postCaptions(summary, title, language,
|
||||
value, csrfTokenClient.tokenBlocking).map { it.success }
|
||||
value, csrfTokenClient.getTokenBlocking()
|
||||
).map { it.success }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(0)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,4 +168,4 @@ class PageEditClient(
|
|||
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import fr.free.nrw.commons.wikidata.model.Entities
|
||||
import fr.free.nrw.commons.wikidata.model.edit.Edit
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.dataclient.Service
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||
import org.wikipedia.edit.Edit
|
||||
import org.wikipedia.wikidata.Entities
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +27,7 @@ interface PageEditInterface {
|
|||
*/
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
fun postEdit(
|
||||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
|
|
@ -36,6 +36,33 @@ interface PageEditInterface {
|
|||
@Field("token") token: String
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
* This method creates or edits a page for nearby items.
|
||||
*
|
||||
* @param title Title of the page to edit. Cannot be used together with pageid.
|
||||
* @param summary Edit summary. Also used as the section title when section=new and sectiontitle is not set.
|
||||
* @param text Text of the page.
|
||||
* @param contentformat Format of the content (e.g., "text/x-wiki").
|
||||
* @param contentmodel Model of the content (e.g., "wikitext").
|
||||
* @param minor Whether the edit is a minor edit.
|
||||
* @param recreate Whether to recreate the page if it does not exist.
|
||||
* @param token A "csrf" token. This should always be sent as the last field of form data.
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
fun postCreate(
|
||||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
@Field("text") text: String,
|
||||
@Field("contentformat") contentformat: String,
|
||||
@Field("contentmodel") contentmodel: String,
|
||||
@Field("minor") minor: Boolean,
|
||||
@Field("recreate") recreate: Boolean,
|
||||
// NOTE: This csrf shold always be sent as the last field of form data
|
||||
@Field("token") token: String
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
* This method posts such that the Content which the page
|
||||
* has will be appended with the value being passed to the
|
||||
|
|
@ -47,7 +74,7 @@ interface PageEditInterface {
|
|||
*/
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
fun postAppendEdit(
|
||||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
|
|
@ -66,7 +93,7 @@ interface PageEditInterface {
|
|||
*/
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
fun postPrependEdit(
|
||||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
|
|
@ -74,10 +101,20 @@ interface PageEditInterface {
|
|||
@Field("token") token: String
|
||||
): Observable<Edit>
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=edit§ion=new")
|
||||
fun postNewSection(
|
||||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
@Field("sectiontitle") sectionTitle: String,
|
||||
@Field("text") sectionText: String,
|
||||
@Field("token") token: String
|
||||
): Observable<Edit>
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
||||
@POST(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
||||
fun postCaptions(
|
||||
@Field("summary") summary: String,
|
||||
@Field("title") title: String,
|
||||
|
|
@ -91,10 +128,7 @@ interface PageEditInterface {
|
|||
* @param titles : Name of the file
|
||||
* @return Single<MwQueryResult>
|
||||
*/
|
||||
@GET(
|
||||
Service.MW_API_PREFIX +
|
||||
"action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles="
|
||||
)
|
||||
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||
fun getWikiText(
|
||||
@Query("titles") title: String
|
||||
): Single<MwQueryResponse?>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package fr.free.nrw.commons.actions
|
|||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||
import io.reactivex.Observable
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import org.wikipedia.dataclient.Service
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -17,7 +17,7 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
class ThanksClient @Inject constructor(
|
||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||
@param:Named("commons-service") private val service: Service
|
||||
private val service: ThanksInterface
|
||||
) {
|
||||
/**
|
||||
* Thanks a user for a particular revision
|
||||
|
|
@ -26,11 +26,23 @@ class ThanksClient @Inject constructor(
|
|||
*/
|
||||
fun thank(revisionId: Long): Observable<Boolean> {
|
||||
return try {
|
||||
service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent)
|
||||
.map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(false)
|
||||
service.thank(
|
||||
revisionId.toString(), // Rev
|
||||
null, // Log
|
||||
csrfTokenClient.getTokenBlocking(), // Token
|
||||
CommonsApplication.getInstance().userAgent // Source
|
||||
).map {
|
||||
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
|
||||
}
|
||||
}
|
||||
catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
Observable.error(throwable)
|
||||
}
|
||||
else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Thanks API.
|
||||
* Context:
|
||||
* The Commons Android app lets you thank another contributor who has uploaded a great picture.
|
||||
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||
*/
|
||||
interface ThanksInterface {
|
||||
@FormUrlEncoded
|
||||
@POST(MW_API_PREFIX + "action=thank")
|
||||
fun thank(
|
||||
@Field("rev") rev: String?,
|
||||
@Field("log") log: String?,
|
||||
@Field("token") token: String,
|
||||
@Field("source") source: String?
|
||||
): Observable<MwThankPostResponse?>
|
||||
}
|
||||
|
|
@ -14,10 +14,8 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -26,31 +24,20 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import fr.free.nrw.commons.auth.login.LoginClient;
|
||||
import fr.free.nrw.commons.auth.login.LoginResult;
|
||||
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
|
||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
||||
import java.util.Locale;
|
||||
import org.wikipedia.AppAdapter;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
import org.wikipedia.login.LoginClient;
|
||||
import org.wikipedia.login.LoginClient.LoginCallback;
|
||||
import org.wikipedia.login.LoginResult;
|
||||
import fr.free.nrw.commons.auth.login.LoginCallback;
|
||||
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import butterknife.OnEditorAction;
|
||||
import butterknife.OnFocusChange;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
|
|
@ -58,25 +45,19 @@ import fr.free.nrw.commons.utils.ConfigUtils;
|
|||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
|
||||
import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey;
|
||||
import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey;
|
||||
|
||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
||||
WikiSite commonsWikiSite;
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore applicationKvStore;
|
||||
|
|
@ -87,39 +68,16 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
@Inject
|
||||
SystemThemeUtils systemThemeUtils;
|
||||
|
||||
@BindView(R.id.login_button)
|
||||
Button loginButton;
|
||||
|
||||
@BindView(R.id.login_username)
|
||||
EditText usernameEdit;
|
||||
|
||||
@BindView(R.id.login_password)
|
||||
EditText passwordEdit;
|
||||
|
||||
@BindView(R.id.login_two_factor)
|
||||
EditText twoFactorEdit;
|
||||
|
||||
@BindView(R.id.error_message_container)
|
||||
ViewGroup errorMessageContainer;
|
||||
|
||||
@BindView(R.id.error_message)
|
||||
TextView errorMessage;
|
||||
|
||||
@BindView(R.id.login_credentials)
|
||||
TextView loginCredentials;
|
||||
|
||||
@BindView(R.id.two_factor_container)
|
||||
TextInputLayout twoFactorContainer;
|
||||
|
||||
private ActivityLoginBinding binding;
|
||||
ProgressDialog progressDialog;
|
||||
private AppCompatDelegate delegate;
|
||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private Call<MwQueryResponse> loginToken;
|
||||
final String saveProgressDailog="ProgressDailog_state";
|
||||
final String saveErrorMessage ="errorMessage";
|
||||
final String saveUsername="username";
|
||||
final String savePassword="password";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -133,31 +91,50 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_login);
|
||||
binding = ActivityLoginBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
ButterKnife.bind(this);
|
||||
String message = getIntent().getStringExtra(loginMessageIntentKey);
|
||||
String username = getIntent().getStringExtra(loginUsernameIntentKey);
|
||||
|
||||
usernameEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.addTextChangedListener(textWatcher);
|
||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||
binding.loginUsername.addTextChangedListener(textWatcher);
|
||||
binding.loginPassword.addTextChangedListener(textWatcher);
|
||||
binding.loginTwoFactor.addTextChangedListener(textWatcher);
|
||||
|
||||
binding.skipLogin.setOnClickListener(view -> skipLogin());
|
||||
binding.forgotPassword.setOnClickListener(view -> forgotPassword());
|
||||
binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked());
|
||||
binding.signUpButton.setOnClickListener(view -> signUp());
|
||||
binding.loginButton.setOnClickListener(view -> performLogin());
|
||||
|
||||
binding.loginPassword.setOnEditorActionListener(this::onEditorAction);
|
||||
binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged);
|
||||
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
loginCredentials.setText(getString(R.string.login_credential));
|
||||
binding.loginCredentials.setText(getString(R.string.login_credential));
|
||||
} else {
|
||||
loginCredentials.setVisibility(View.GONE);
|
||||
binding.loginCredentials.setVisibility(View.GONE);
|
||||
}
|
||||
if (message != null) {
|
||||
showMessage(message, R.color.secondaryDarkColor);
|
||||
}
|
||||
if (username != null) {
|
||||
binding.loginUsername.setText(username);
|
||||
}
|
||||
}
|
||||
|
||||
@OnFocusChange(R.id.login_password)
|
||||
/**
|
||||
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
|
||||
* @param view The keyboard
|
||||
* @param hasFocus Set to true if the keyboard has focus
|
||||
*/
|
||||
void onPasswordFocusChanged(View view, boolean hasFocus) {
|
||||
if (!hasFocus) {
|
||||
ViewUtil.hideKeyboard(view);
|
||||
}
|
||||
}
|
||||
|
||||
@OnEditorAction(R.id.login_password)
|
||||
boolean onEditorAction(int actionId, KeyEvent keyEvent) {
|
||||
if (loginButton.isEnabled()) {
|
||||
boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||
if (binding.loginButton.isEnabled()) {
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
performLogin();
|
||||
return true;
|
||||
|
|
@ -170,8 +147,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
|
||||
@OnClick(R.id.skip_login)
|
||||
void skipLogin() {
|
||||
protected void skipLogin() {
|
||||
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
|
||||
.setMessage(R.string.skip_login_message)
|
||||
.setCancelable(false)
|
||||
|
|
@ -183,18 +159,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
.show();
|
||||
}
|
||||
|
||||
@OnClick(R.id.forgot_password)
|
||||
void forgotPassword() {
|
||||
protected void forgotPassword() {
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
||||
}
|
||||
|
||||
@OnClick(R.id.about_privacy_policy)
|
||||
void onPrivacyPolicyClicked() {
|
||||
protected void onPrivacyPolicyClicked() {
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
||||
}
|
||||
|
||||
@OnClick(R.id.sign_up_button)
|
||||
void signUp() {
|
||||
protected void signUp() {
|
||||
Intent intent = new Intent(this, SignupActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
|
@ -232,76 +205,65 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
usernameEdit.removeTextChangedListener(textWatcher);
|
||||
passwordEdit.removeTextChangedListener(textWatcher);
|
||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
||||
binding.loginUsername.removeTextChangedListener(textWatcher);
|
||||
binding.loginPassword.removeTextChangedListener(textWatcher);
|
||||
binding.loginTwoFactor.removeTextChangedListener(textWatcher);
|
||||
delegate.onDestroy();
|
||||
if(null!=loginClient) {
|
||||
loginClient.cancel();
|
||||
}
|
||||
binding = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@OnClick(R.id.login_button)
|
||||
public void performLogin() {
|
||||
Timber.d("Login to start!");
|
||||
final String username = usernameEdit.getText().toString();
|
||||
final String rawUsername = usernameEdit.getText().toString().trim();
|
||||
final String password = passwordEdit.getText().toString();
|
||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
||||
final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString();
|
||||
final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString();
|
||||
final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString();
|
||||
|
||||
showLoggingProgressBar();
|
||||
doLogin(username, password, twoFactorCode);
|
||||
loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(),
|
||||
new LoginCallback() {
|
||||
@Override
|
||||
public void success(@NonNull LoginResult loginResult) {
|
||||
runOnUiThread(()->{
|
||||
Timber.d("Login Success");
|
||||
hideProgress();
|
||||
onLoginSuccess(loginResult);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
|
||||
runOnUiThread(()->{
|
||||
Timber.d("Requesting 2FA prompt");
|
||||
hideProgress();
|
||||
askUserForTwoFactorAuth();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passwordResetPrompt(@Nullable String token) {
|
||||
runOnUiThread(()->{
|
||||
Timber.d("Showing password reset prompt");
|
||||
hideProgress();
|
||||
showPasswordResetPrompt();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(@NonNull Throwable caught) {
|
||||
runOnUiThread(()->{
|
||||
Timber.e(caught);
|
||||
hideProgress();
|
||||
showMessageAndCancelDialog(caught.getLocalizedMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doLogin(String username, String password, String twoFactorCode) {
|
||||
progressDialog.show();
|
||||
loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken();
|
||||
loginToken.enqueue(
|
||||
new Callback<MwQueryResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<MwQueryResponse> call,
|
||||
Response<MwQueryResponse> response) {
|
||||
loginClient.login(commonsWikiSite, username, password, null, twoFactorCode,
|
||||
response.body().query().loginToken(), Locale.getDefault().getLanguage(), new LoginCallback() {
|
||||
@Override
|
||||
public void success(@NonNull LoginResult result) {
|
||||
Timber.d("Login Success");
|
||||
onLoginSuccess(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void twoFactorPrompt(@NonNull Throwable caught,
|
||||
@Nullable String token) {
|
||||
Timber.d("Requesting 2FA prompt");
|
||||
hideProgress();
|
||||
askUserForTwoFactorAuth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passwordResetPrompt(@Nullable String token) {
|
||||
Timber.d("Showing password reset prompt");
|
||||
hideProgress();
|
||||
showPasswordResetPrompt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(@NonNull Throwable caught) {
|
||||
Timber.e(caught);
|
||||
hideProgress();
|
||||
showMessageAndCancelDialog(caught.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<MwQueryResponse> call, Throwable t) {
|
||||
Timber.e(t);
|
||||
showMessageAndCancelDialog(t.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void hideProgress() {
|
||||
progressDialog.dismiss();
|
||||
|
|
@ -332,13 +294,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
private void onLoginSuccess(LoginResult loginResult) {
|
||||
if (!progressDialog.isShowing()) {
|
||||
// no longer attached to activity!
|
||||
return;
|
||||
}
|
||||
compositeDisposable.clear();
|
||||
sessionManager.setUserLoggedIn(true);
|
||||
AppAdapter.get().updateAccount(loginResult);
|
||||
sessionManager.updateAccount(loginResult);
|
||||
progressDialog.dismiss();
|
||||
showSuccessAndDismissDialog();
|
||||
startMainActivity();
|
||||
|
|
@ -385,9 +343,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
progressDialog.dismiss();
|
||||
twoFactorContainer.setVisibility(VISIBLE);
|
||||
twoFactorEdit.setVisibility(VISIBLE);
|
||||
twoFactorEdit.requestFocus();
|
||||
binding.twoFactorContainer.setVisibility(VISIBLE);
|
||||
binding.loginTwoFactor.setVisibility(VISIBLE);
|
||||
binding.loginTwoFactor.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
||||
|
|
@ -418,15 +376,15 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||
errorMessage.setText(getString(resId));
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
errorMessageContainer.setVisibility(VISIBLE);
|
||||
binding.errorMessage.setText(getString(resId));
|
||||
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
binding.errorMessageContainer.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private void showMessage(String message, @ColorRes int colorResId) {
|
||||
errorMessage.setText(message);
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
errorMessageContainer.setVisibility(VISIBLE);
|
||||
binding.errorMessage.setText(message);
|
||||
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
binding.errorMessageContainer.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
|
|
@ -447,9 +405,11 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
||||
loginButton.setEnabled(enabled);
|
||||
boolean enabled = binding.loginUsername.getText().length() != 0 &&
|
||||
binding.loginPassword.getText().length() != 0 &&
|
||||
(BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 ||
|
||||
binding.loginTwoFactor.getVisibility() != VISIBLE);
|
||||
binding.loginButton.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -467,22 +427,22 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
} else {
|
||||
outState.putBoolean(saveProgressDailog,false);
|
||||
}
|
||||
outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage
|
||||
outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage
|
||||
outState.putString(saveUsername,getUsername()); // Save the username
|
||||
outState.putString(savePassword,getPassword()); // Save the password
|
||||
}
|
||||
private String getUsername() {
|
||||
return usernameEdit.getText().toString();
|
||||
return binding.loginUsername.getText().toString();
|
||||
}
|
||||
private String getPassword(){
|
||||
return passwordEdit.getText().toString();
|
||||
return binding.loginPassword.getText().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
usernameEdit.setText(savedInstanceState.getString(saveUsername));
|
||||
passwordEdit.setText(savedInstanceState.getString(savePassword));
|
||||
binding.loginUsername.setText(savedInstanceState.getString(saveUsername));
|
||||
binding.loginPassword.setText(savedInstanceState.getString(savePassword));
|
||||
if(savedInstanceState.getBoolean(saveProgressDailog)) {
|
||||
performLogin();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
|
||||
/**
|
||||
* Handler for logout
|
||||
*/
|
||||
@Singleton
|
||||
public class LogoutClient {
|
||||
|
||||
private final Service service;
|
||||
|
||||
@Inject
|
||||
public LogoutClient(@Named("commons-service") Service service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the CSRF token and uses that to post the logout api call
|
||||
* @return
|
||||
*/
|
||||
public Observable<MwPostResponse> postLogout() {
|
||||
return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout(
|
||||
Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken())));
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,7 @@ import android.text.TextUtils;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.login.LoginResult;
|
||||
|
||||
import fr.free.nrw.commons.auth.login.LoginResult;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
|
@ -123,18 +122,18 @@ public class SessionManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* 1. Clears existing accounts from account manager
|
||||
* 2. Calls MediaWikiApi's logout function to clear cookies
|
||||
* @return
|
||||
* Returns a Completable that clears existing accounts from account manager
|
||||
*/
|
||||
public Completable logout() {
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
||||
return Completable.fromObservable(Observable.fromArray(allAccounts)
|
||||
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
|
||||
.doOnComplete(() -> {
|
||||
currentAccount = null;
|
||||
});
|
||||
return Completable.fromObservable(
|
||||
Observable.empty()
|
||||
.doOnComplete(
|
||||
() -> {
|
||||
removeAccount();
|
||||
currentAccount = null;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
|
@ -61,4 +63,20 @@ public class SignupActivity extends BaseActivity {
|
|||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
|
||||
* https://issuetracker.google.com/issues/141132133
|
||||
* App tries to put light/dark theme to webview and crashes in the process
|
||||
* This code tries to prevent applying the theme when sdk is between api 21 to 25
|
||||
* @param overrideConfiguration
|
||||
*/
|
||||
@Override
|
||||
public void applyOverrideConfiguration(final Configuration overrideConfiguration) {
|
||||
if (Build.VERSION.SDK_INT <= 25 &&
|
||||
(getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) {
|
||||
return;
|
||||
}
|
||||
super.applyOverrideConfiguration(overrideConfiguration);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
package fr.free.nrw.commons.auth.csrf
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import fr.free.nrw.commons.auth.login.LoginClient
|
||||
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executors.newSingleThreadExecutor
|
||||
|
||||
class CsrfTokenClient(
|
||||
private val sessionManager: SessionManager,
|
||||
private val csrfTokenInterface: CsrfTokenInterface,
|
||||
private val loginClient: LoginClient,
|
||||
private val logoutClient: LogoutClient
|
||||
) {
|
||||
private var retries = 0
|
||||
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
fun getTokenBlocking(): String {
|
||||
var token = ""
|
||||
val userName = sessionManager.userName ?: ""
|
||||
val password = sessionManager.password ?: ""
|
||||
|
||||
for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) {
|
||||
try {
|
||||
if (retry > 0) {
|
||||
// Log in explicitly
|
||||
loginClient.loginBlocking(userName, password, "")
|
||||
}
|
||||
|
||||
// Get CSRFToken response off the main thread.
|
||||
val response = newSingleThreadExecutor().submit(Callable {
|
||||
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||
}).get()
|
||||
|
||||
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
token = response.body()!!.query()!!.csrfToken()!!
|
||||
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
break
|
||||
} catch (e: LoginFailedException) {
|
||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
Timber.w(t)
|
||||
}
|
||||
}
|
||||
|
||||
if (token.isEmpty() || token == ANON_TOKEN) {
|
||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
|
||||
requestToken(service, object : Callback {
|
||||
override fun success(token: String?) {
|
||||
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||
retryWithLogin(cb) {
|
||||
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
} else {
|
||||
cb.success(token)
|
||||
}
|
||||
}
|
||||
|
||||
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||
|
||||
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||
})
|
||||
|
||||
@VisibleForTesting
|
||||
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
|
||||
val call = service.getCsrfTokenCall()
|
||||
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
|
||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.success(response.body()!!.query()!!.csrfToken())
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.failure(t)
|
||||
}
|
||||
})
|
||||
return call
|
||||
}
|
||||
|
||||
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
|
||||
val userName = sessionManager.userName
|
||||
val password = sessionManager.password
|
||||
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||
retries++
|
||||
logoutClient.logout()
|
||||
login(userName, password, callback) {
|
||||
Timber.i("retrying...")
|
||||
cancel()
|
||||
csrfTokenCall = request(csrfTokenInterface, callback)
|
||||
}
|
||||
} else {
|
||||
callback.failure(caught())
|
||||
}
|
||||
}
|
||||
|
||||
private fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
callback: Callback,
|
||||
retryCallback: () -> Unit
|
||||
) = loginClient.request(username, password, object : LoginCallback {
|
||||
override fun success(loginResult: LoginResult) {
|
||||
if (loginResult.pass) {
|
||||
sessionManager.updateAccount(loginResult)
|
||||
retryCallback()
|
||||
} else {
|
||||
callback.failure(LoginFailedException(loginResult.message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun twoFactorPrompt(caught: Throwable, token: String?) =
|
||||
callback.twoFactorPrompt()
|
||||
|
||||
// Should not happen here, but call the callback just in case.
|
||||
override fun passwordResetPrompt(token: String?) =
|
||||
callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||
|
||||
override fun error(caught: Throwable) = callback.failure(caught)
|
||||
})
|
||||
|
||||
private fun cancel() {
|
||||
loginClient.cancel()
|
||||
if (csrfTokenCall != null) {
|
||||
csrfTokenCall!!.cancel()
|
||||
csrfTokenCall = null
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun success(token: String?)
|
||||
fun failure(caught: Throwable?)
|
||||
fun twoFactorPrompt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ANON_TOKEN = "+\\"
|
||||
private const val MAX_RETRIES = 1
|
||||
private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2
|
||||
const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure."
|
||||
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
|
||||
}
|
||||
}
|
||||
class InvalidLoginTokenException(message: String) : Exception(message)
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package fr.free.nrw.commons.auth.csrf
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
|
||||
interface CsrfTokenInterface {
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||
fun getCsrfTokenCall(): Call<MwQueryResponse?>
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package fr.free.nrw.commons.auth.csrf
|
||||
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
|
||||
import javax.inject.Inject
|
||||
|
||||
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) {
|
||||
fun logout() = store.clear()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
interface LoginCallback {
|
||||
fun success(loginResult: LoginResult)
|
||||
fun twoFactorPrompt(caught: Throwable, token: String?)
|
||||
fun passwordResetPrompt(token: String?)
|
||||
fun error(caught: Throwable)
|
||||
}
|
||||
193
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
193
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
import android.text.TextUtils
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Responsible for making login related requests to the server.
|
||||
*/
|
||||
class LoginClient(private val loginInterface: LoginInterface) {
|
||||
private var tokenCall: Call<MwQueryResponse?>? = null
|
||||
private var loginCall: Call<LoginResponse?>? = null
|
||||
|
||||
/**
|
||||
* userLanguage
|
||||
* It holds the value of the user's device language code.
|
||||
* For example, if user's device language is English it will hold En
|
||||
* The value will be fetched when the user clicks Login Button in the LoginActivity
|
||||
*/
|
||||
private var userLanguage = ""
|
||||
|
||||
private fun getLoginToken() = loginInterface.getLoginToken()
|
||||
|
||||
fun request(userName: String, password: String, cb: LoginCallback) {
|
||||
cancel()
|
||||
|
||||
tokenCall = getLoginToken()
|
||||
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
|
||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||
login(
|
||||
userName, password, null, null, response.body()!!.query()!!.loginToken(),
|
||||
userLanguage, cb
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.error(caught)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun login(
|
||||
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
|
||||
loginToken: String?, userLanguage: String, cb: LoginCallback
|
||||
) {
|
||||
this.userLanguage = userLanguage
|
||||
|
||||
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
|
||||
)
|
||||
}
|
||||
|
||||
loginCall!!.enqueue(object : Callback<LoginResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<LoginResponse?>,
|
||||
response: Response<LoginResponse?>
|
||||
) {
|
||||
val loginResult = response.body()?.toLoginResult(password)
|
||||
if (loginResult != null) {
|
||||
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||
// The server could do some transformations on user names, e.g. on some
|
||||
// wikis is uppercases the first letter.
|
||||
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||
} else if ("UI" == loginResult.status) {
|
||||
when (loginResult) {
|
||||
is OAuthResult -> cb.twoFactorPrompt(
|
||||
LoginFailedException(loginResult.message),
|
||||
loginToken
|
||||
)
|
||||
|
||||
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||
|
||||
is LoginResult.Result -> cb.error(
|
||||
LoginFailedException(loginResult.message)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cb.error(LoginFailedException(loginResult.message))
|
||||
}
|
||||
} else {
|
||||
cb.error(IOException("Login failed. Unexpected response."))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.error(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun doLogin(
|
||||
username: String,
|
||||
password: String,
|
||||
twoFactorCode: String,
|
||||
userLanguage: String,
|
||||
loginCallback: LoginCallback
|
||||
) {
|
||||
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{
|
||||
override fun onResponse(
|
||||
call: Call<MwQueryResponse?>,
|
||||
response: Response<MwQueryResponse?>
|
||||
) = if (response.isSuccessful){
|
||||
val loginToken = response.body()?.query()?.loginToken()
|
||||
loginToken?.let {
|
||||
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
||||
} ?: run {
|
||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||
}
|
||||
} else {
|
||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||
loginCallback.error(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
@Throws(Throwable::class)
|
||||
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
|
||||
val tokenResponse = getLoginToken().execute()
|
||||
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
|
||||
throw IOException("Unexpected response when getting login token.")
|
||||
}
|
||||
|
||||
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName, password, null, twoFactorCode, loginToken, userLanguage, true
|
||||
)
|
||||
}
|
||||
|
||||
val response = tempLoginCall.execute()
|
||||
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
||||
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
|
||||
|
||||
if ("UI" == loginResult.status) {
|
||||
if (loginResult is OAuthResult) {
|
||||
// TODO: Find a better way to boil up the warning about 2FA
|
||||
throw LoginFailedException(loginResult.message)
|
||||
}
|
||||
throw LoginFailedException(loginResult.message)
|
||||
}
|
||||
|
||||
if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) {
|
||||
throw LoginFailedException(loginResult.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) =
|
||||
loginInterface.getUserInfo(userName)
|
||||
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response: MwQueryResponse? ->
|
||||
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||
loginResult.groups =
|
||||
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
||||
cb.success(loginResult)
|
||||
}, { caught: Throwable ->
|
||||
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||
cb.error(caught)
|
||||
})
|
||||
|
||||
fun cancel() {
|
||||
tokenCall?.let {
|
||||
it.cancel()
|
||||
tokenCall = null
|
||||
}
|
||||
|
||||
loginCall?.let {
|
||||
it.cancel()
|
||||
loginCall = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
class LoginFailedException(message: String?) : Throwable(message)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import io.reactivex.Observable
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface LoginInterface {
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=login")
|
||||
fun getLoginToken(): Call<MwQueryResponse?>
|
||||
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@FormUrlEncoded
|
||||
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||
fun postLogIn(
|
||||
@Field("username") user: String?,
|
||||
@Field("password") pass: String?,
|
||||
@Field("logintoken") token: String?,
|
||||
@Field("uselang") userLanguage: String?,
|
||||
@Field("loginreturnurl") url: String?
|
||||
): Call<LoginResponse?>
|
||||
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@FormUrlEncoded
|
||||
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||
fun postLogIn(
|
||||
@Field("username") user: String?,
|
||||
@Field("password") pass: String?,
|
||||
@Field("retype") retypedPass: String?,
|
||||
@Field("OATHToken") twoFactorCode: String?,
|
||||
@Field("logintoken") token: String?,
|
||||
@Field("uselang") userLanguage: String?,
|
||||
@Field("logincontinue") loginContinue: Boolean
|
||||
): Call<LoginResponse?>
|
||||
|
||||
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
||||
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
||||
|
||||
class LoginResponse {
|
||||
@SerializedName("error")
|
||||
val error: MwServiceError? = null
|
||||
|
||||
@SerializedName("clientlogin")
|
||||
private val clientLogin: ClientLogin? = null
|
||||
|
||||
fun toLoginResult(password: String): LoginResult? {
|
||||
return clientLogin?.toLoginResult(password)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ClientLogin {
|
||||
private val status: String? = null
|
||||
private val requests: List<Request>? = null
|
||||
private val message: String? = null
|
||||
|
||||
@SerializedName("username")
|
||||
private val userName: String? = null
|
||||
|
||||
fun toLoginResult(password: String): LoginResult {
|
||||
var userMessage = message
|
||||
if ("UI" == status) {
|
||||
if (requests != null) {
|
||||
for (req in requests) {
|
||||
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
|
||||
return OAuthResult(status, userName, password, message)
|
||||
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
|
||||
return ResetPasswordResult(status, userName, password, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("PASS" != status && "FAIL" != status) {
|
||||
//TODO: String resource -- Looks like needed for others in this class too
|
||||
userMessage = "An unknown error occurred."
|
||||
}
|
||||
return Result(status ?: "", userName, password, userMessage)
|
||||
}
|
||||
}
|
||||
|
||||
internal class Request {
|
||||
private val id: String? = null
|
||||
private val required: String? = null
|
||||
private val provider: String? = null
|
||||
private val account: String? = null
|
||||
private val fields: Map<String, RequestField>? = null
|
||||
|
||||
fun id(): String? = id
|
||||
}
|
||||
|
||||
internal class RequestField {
|
||||
private val type: String? = null
|
||||
private val label: String? = null
|
||||
private val help: String? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
sealed class LoginResult(
|
||||
val status: String,
|
||||
val userName: String?,
|
||||
val password: String?,
|
||||
val message: String?
|
||||
) {
|
||||
var userId = 0
|
||||
var groups = emptySet<String>()
|
||||
val pass: Boolean get() = "PASS" == status
|
||||
|
||||
class Result(
|
||||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
): LoginResult(status, userName, password, message)
|
||||
|
||||
class OAuthResult(
|
||||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
) : LoginResult(status, userName, password, message)
|
||||
|
||||
class ResetPasswordResult(
|
||||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
) : LoginResult(status, userName, password, message)
|
||||
}
|
||||
|
|
@ -5,23 +5,15 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.ParentViewPager;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -29,12 +21,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
private FragmentManager supportFragmentManager;
|
||||
private BookmarksPagerAdapter adapter;
|
||||
@BindView(R.id.viewPagerBookmarks)
|
||||
ParentViewPager viewPager;
|
||||
@BindView(R.id.tab_layout)
|
||||
TabLayout tabLayout;
|
||||
@BindView(R.id.fragmentContainer)
|
||||
FrameLayout fragmentContainer;
|
||||
FragmentBookmarksBinding binding;
|
||||
|
||||
@Inject
|
||||
ContributionController controller;
|
||||
|
|
@ -54,7 +41,9 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
public void setScroll(boolean canScroll) {
|
||||
viewPager.setCanScroll(canScroll);
|
||||
if (binding!=null) {
|
||||
binding.viewPagerBookmarks.setCanScroll(canScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -68,8 +57,7 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
binding = FragmentBookmarksBinding.inflate(inflater, container, false);
|
||||
|
||||
// Activity can call methods in the fragment by acquiring a
|
||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||
|
|
@ -77,14 +65,14 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
|
||||
applicationKvStore.getBoolean("login_skipped"));
|
||||
viewPager.setAdapter(adapter);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
binding.viewPagerBookmarks.setAdapter(adapter);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks);
|
||||
|
||||
((MainActivity) getActivity()).showTabs();
|
||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
|
||||
setupTabLayout();
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,15 +80,15 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
* visibility of tabLayout to gone.
|
||||
*/
|
||||
public void setupTabLayout() {
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
if (adapter.getCount() == 1) {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void onBackPressed() {
|
||||
if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition())))
|
||||
if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition())))
|
||||
.backPressed()) {
|
||||
// The event is handled internally by the adapter , no further action required.
|
||||
return;
|
||||
|
|
@ -108,4 +96,10 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|||
// Event is not handled by the adapter ( performed back action ) change action bar.
|
||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
||||
|
|
@ -22,6 +20,7 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
|||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.navtab.NavTab;
|
||||
|
|
@ -39,8 +38,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
public Fragment listFragment;
|
||||
private BookmarksPagerAdapter bookmarksPagerAdapter;
|
||||
|
||||
@BindView(R.id.explore_container)
|
||||
FrameLayout container;
|
||||
FragmentFeaturedRootBinding binding;
|
||||
|
||||
public BookmarkListRootFragment() {
|
||||
//empty constructor necessary otherwise crashes on recreate
|
||||
|
|
@ -70,9 +68,8 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
return view;
|
||||
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -184,7 +181,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
public void refreshNominatedMedia(int index) {
|
||||
if (mediaDetails != null && !listFragment.isVisible()) {
|
||||
removeFragment(mediaDetails);
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
||||
setFragment(mediaDetails, listFragment);
|
||||
mediaDetails.showImage(index);
|
||||
|
|
@ -241,9 +238,9 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Log.d("deneme8", "on media clicked");
|
||||
container.setVisibility(View.VISIBLE);
|
||||
((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
||||
setFragment(mediaDetails, listFragment);
|
||||
mediaDetails.showImage(position);
|
||||
|
|
@ -253,4 +250,10 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
public void onBackStackChanged() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentPagerAdapter;
|
|||
import java.util.ArrayList;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
||||
|
||||
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -26,17 +25,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
*/
|
||||
public class BookmarkItemsFragment extends DaggerFragment {
|
||||
|
||||
@BindView(R.id.status_message)
|
||||
TextView statusTextView;
|
||||
|
||||
@BindView(R.id.loading_images_progress_bar)
|
||||
ProgressBar progressBar;
|
||||
|
||||
@BindView(R.id.list_view)
|
||||
RecyclerView recyclerView;
|
||||
|
||||
@BindView(R.id.parent_layout)
|
||||
RelativeLayout parentLayout;
|
||||
private FragmentBookmarksItemsBinding binding;
|
||||
|
||||
@Inject
|
||||
BookmarkItemsController controller;
|
||||
|
|
@ -51,16 +40,13 @@ public class BookmarkItemsFragment extends DaggerFragment {
|
|||
final ViewGroup container,
|
||||
final Bundle savedInstanceState
|
||||
) {
|
||||
final View v = inflater.inflate(R.layout.fragment_bookmarks_items, container, false);
|
||||
ButterKnife.bind(this, v);
|
||||
return v;
|
||||
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
initList(requireContext());
|
||||
}
|
||||
|
||||
|
|
@ -77,13 +63,19 @@ public class BookmarkItemsFragment extends DaggerFragment {
|
|||
private void initList(final Context context) {
|
||||
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
|
||||
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
|
||||
recyclerView.setAdapter(adapter);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
binding.listView.setAdapter(adapter);
|
||||
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
||||
if (depictItems.isEmpty()) {
|
||||
statusTextView.setText(R.string.bookmark_empty);
|
||||
statusTextView.setVisibility(View.VISIBLE);
|
||||
binding.statusMessage.setText(R.string.bookmark_empty);
|
||||
binding.statusMessage.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
statusTextView.setVisibility(View.GONE);
|
||||
binding.statusMessage.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@ public class BookmarkLocationsDao {
|
|||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
BookmarkLocationsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
null);
|
||||
BookmarkLocationsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
items.add(fromCursor(cursor));
|
||||
}
|
||||
|
|
@ -126,11 +126,11 @@ public class BookmarkLocationsDao {
|
|||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
BookmarkLocationsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{bookmarkLocation.name},
|
||||
null);
|
||||
BookmarkLocationsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{bookmarkLocation.name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ public class BookmarkLocationsDao {
|
|||
@NonNull
|
||||
Place fromCursor(final Cursor cursor) {
|
||||
final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)),
|
||||
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
|
||||
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
|
||||
|
||||
final Sitelinks.Builder builder = new Sitelinks.Builder();
|
||||
builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK)));
|
||||
|
|
@ -207,40 +207,40 @@ public class BookmarkLocationsDao {
|
|||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_NAME,
|
||||
COLUMN_LANGUAGE,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_CATEGORY,
|
||||
COLUMN_LABEL_TEXT,
|
||||
COLUMN_LABEL_ICON,
|
||||
COLUMN_LAT,
|
||||
COLUMN_LONG,
|
||||
COLUMN_IMAGE_URL,
|
||||
COLUMN_WIKIPEDIA_LINK,
|
||||
COLUMN_WIKIDATA_LINK,
|
||||
COLUMN_COMMONS_LINK,
|
||||
COLUMN_PIC,
|
||||
COLUMN_EXISTS,
|
||||
COLUMN_NAME,
|
||||
COLUMN_LANGUAGE,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_CATEGORY,
|
||||
COLUMN_LABEL_TEXT,
|
||||
COLUMN_LABEL_ICON,
|
||||
COLUMN_LAT,
|
||||
COLUMN_LONG,
|
||||
COLUMN_IMAGE_URL,
|
||||
COLUMN_WIKIPEDIA_LINK,
|
||||
COLUMN_WIKIDATA_LINK,
|
||||
COLUMN_COMMONS_LINK,
|
||||
COLUMN_PIC,
|
||||
COLUMN_EXISTS,
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_NAME + " STRING PRIMARY KEY,"
|
||||
+ COLUMN_LANGUAGE + " STRING,"
|
||||
+ COLUMN_DESCRIPTION + " STRING,"
|
||||
+ COLUMN_CATEGORY + " STRING,"
|
||||
+ COLUMN_LABEL_TEXT + " STRING,"
|
||||
+ COLUMN_LABEL_ICON + " INTEGER,"
|
||||
+ COLUMN_LAT + " DOUBLE,"
|
||||
+ COLUMN_LONG + " DOUBLE,"
|
||||
+ COLUMN_IMAGE_URL + " STRING,"
|
||||
+ COLUMN_WIKIPEDIA_LINK + " STRING,"
|
||||
+ COLUMN_WIKIDATA_LINK + " STRING,"
|
||||
+ COLUMN_COMMONS_LINK + " STRING,"
|
||||
+ COLUMN_PIC + " STRING,"
|
||||
+ COLUMN_EXISTS + " STRING"
|
||||
+ ");";
|
||||
+ COLUMN_NAME + " STRING PRIMARY KEY,"
|
||||
+ COLUMN_LANGUAGE + " STRING,"
|
||||
+ COLUMN_DESCRIPTION + " STRING,"
|
||||
+ COLUMN_CATEGORY + " STRING,"
|
||||
+ COLUMN_LABEL_TEXT + " STRING,"
|
||||
+ COLUMN_LABEL_ICON + " INTEGER,"
|
||||
+ COLUMN_LAT + " DOUBLE,"
|
||||
+ COLUMN_LONG + " DOUBLE,"
|
||||
+ COLUMN_IMAGE_URL + " STRING,"
|
||||
+ COLUMN_WIKIPEDIA_LINK + " STRING,"
|
||||
+ COLUMN_WIKIDATA_LINK + " STRING,"
|
||||
+ COLUMN_COMMONS_LINK + " STRING,"
|
||||
+ COLUMN_PIC + " STRING,"
|
||||
+ COLUMN_EXISTS + " STRING"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
|
|
@ -308,4 +308,4 @@ public class BookmarkLocationsDao {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,57 @@
|
|||
package fr.free.nrw.commons.bookmarks.locations;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions;
|
||||
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Unit;
|
||||
|
||||
public class BookmarkLocationsFragment extends DaggerFragment {
|
||||
|
||||
@BindView(R.id.statusMessage) TextView statusTextView;
|
||||
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
|
||||
@BindView(R.id.listView) RecyclerView recyclerView;
|
||||
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
|
||||
public FragmentBookmarksLocationsBinding binding;
|
||||
|
||||
@Inject BookmarkLocationsController controller;
|
||||
@Inject ContributionController contributionController;
|
||||
@Inject BookmarkLocationsDao bookmarkLocationDao;
|
||||
@Inject CommonPlaceClickActions commonPlaceClickActions;
|
||||
private PlaceAdapter adapter;
|
||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
@Override
|
||||
public void onActivityResult(Map<String, Boolean> result) {
|
||||
boolean areAllGranted = true;
|
||||
for(final boolean b : result.values()) {
|
||||
areAllGranted = areAllGranted && b;
|
||||
}
|
||||
|
||||
if (areAllGranted) {
|
||||
contributionController.locationPermissionCallback.onLocationPermissionGranted();
|
||||
} else {
|
||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||
} else {
|
||||
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an instance of the fragment with the right bundle parameters
|
||||
|
|
@ -51,25 +67,25 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
ViewGroup container,
|
||||
Bundle savedInstanceState
|
||||
) {
|
||||
View v = inflater.inflate(R.layout.fragment_bookmarks_locations, container, false);
|
||||
ButterKnife.bind(this, v);
|
||||
return v;
|
||||
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.loadingImagesProgressBar.setVisibility(View.VISIBLE);
|
||||
binding.listView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
adapter = new PlaceAdapter(bookmarkLocationDao,
|
||||
place -> Unit.INSTANCE,
|
||||
(place, isBookmarked) -> {
|
||||
adapter.remove(place);
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
commonPlaceClickActions
|
||||
commonPlaceClickActions,
|
||||
inAppCameraLocationPermissionLauncher
|
||||
);
|
||||
recyclerView.setAdapter(adapter);
|
||||
binding.listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -84,12 +100,12 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
private void initList() {
|
||||
List<Place> places = controller.loadFavoritesLocations();
|
||||
adapter.setItems(places);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
||||
if (places.size() <= 0) {
|
||||
statusTextView.setText(R.string.bookmark_empty);
|
||||
statusTextView.setVisibility(View.VISIBLE);
|
||||
binding.statusMessage.setText(R.string.bookmark_empty);
|
||||
binding.statusMessage.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
statusTextView.setVisibility(View.GONE);
|
||||
binding.statusMessage.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,4 +113,10 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,20 +9,15 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.GridView;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
|
||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
||||
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -37,11 +32,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
private GridViewAdapter gridAdapter;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
@BindView(R.id.statusMessage) TextView statusTextView;
|
||||
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
|
||||
@BindView(R.id.bookmarkedPicturesList) GridView gridView;
|
||||
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
|
||||
|
||||
private FragmentBookmarksPicturesBinding binding;
|
||||
@Inject
|
||||
BookmarkPicturesController controller;
|
||||
|
||||
|
|
@ -59,15 +50,14 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
ViewGroup container,
|
||||
Bundle savedInstanceState
|
||||
) {
|
||||
View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false);
|
||||
ButterKnife.bind(this, v);
|
||||
return v;
|
||||
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
|
||||
binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
|
||||
initList();
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +71,14 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
compositeDisposable.clear();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (controller.needRefreshBookmarkedPictures()) {
|
||||
gridView.setVisibility(GONE);
|
||||
binding.bookmarkedPicturesList.setVisibility(GONE);
|
||||
if (gridAdapter != null) {
|
||||
gridAdapter.clear();
|
||||
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
|
||||
|
|
@ -107,8 +98,8 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
statusTextView.setVisibility(GONE);
|
||||
binding.loadingImagesProgressBar.setVisibility(VISIBLE);
|
||||
binding.statusMessage.setVisibility(GONE);
|
||||
|
||||
compositeDisposable.add(controller.loadBookmarkedPictures()
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
|
@ -120,12 +111,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
* Handles the UI updates for no internet scenario
|
||||
*/
|
||||
private void handleNoInternet() {
|
||||
progressBar.setVisibility(GONE);
|
||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||
statusTextView.setVisibility(VISIBLE);
|
||||
statusTextView.setText(getString(R.string.no_internet));
|
||||
binding.statusMessage.setVisibility(VISIBLE);
|
||||
binding.statusMessage.setText(getString(R.string.no_internet));
|
||||
} else {
|
||||
ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
|
||||
ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +127,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
private void handleError(Throwable throwable) {
|
||||
Timber.e(throwable, "Error occurred while loading images inside a category");
|
||||
try{
|
||||
ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
|
||||
ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images);
|
||||
initErrorView();
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
|
|
@ -147,12 +138,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
* Handles the UI updates for a error scenario
|
||||
*/
|
||||
private void initErrorView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||
statusTextView.setVisibility(VISIBLE);
|
||||
statusTextView.setText(getString(R.string.no_images_found));
|
||||
binding.statusMessage.setVisibility(VISIBLE);
|
||||
binding.statusMessage.setText(getString(R.string.no_images_found));
|
||||
} else {
|
||||
statusTextView.setVisibility(GONE);
|
||||
binding.statusMessage.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,12 +151,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
* Handles the UI updates when there is no bookmarks
|
||||
*/
|
||||
private void initEmptyBookmarkListView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||
statusTextView.setVisibility(VISIBLE);
|
||||
statusTextView.setText(getString(R.string.bookmark_empty));
|
||||
binding.statusMessage.setVisibility(VISIBLE);
|
||||
binding.statusMessage.setText(getString(R.string.bookmark_empty));
|
||||
} else {
|
||||
statusTextView.setVisibility(GONE);
|
||||
binding.statusMessage.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,18 +179,18 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
setAdapter(collection);
|
||||
} else {
|
||||
if (gridAdapter.containsAll(collection)) {
|
||||
progressBar.setVisibility(GONE);
|
||||
statusTextView.setVisibility(GONE);
|
||||
gridView.setVisibility(VISIBLE);
|
||||
gridView.setAdapter(gridAdapter);
|
||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||
binding.statusMessage.setVisibility(GONE);
|
||||
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
||||
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
||||
return;
|
||||
}
|
||||
gridAdapter.addItems(collection);
|
||||
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
|
||||
}
|
||||
progressBar.setVisibility(GONE);
|
||||
statusTextView.setVisibility(GONE);
|
||||
gridView.setVisibility(VISIBLE);
|
||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
||||
binding.statusMessage.setVisibility(GONE);
|
||||
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -212,7 +203,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
R.layout.layout_category_images,
|
||||
mediaList
|
||||
);
|
||||
gridView.setAdapter(gridAdapter);
|
||||
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,6 +212,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
* @return GridView Adapter
|
||||
*/
|
||||
public ListAdapter getAdapter() {
|
||||
return gridView.getAdapter();
|
||||
return binding.bookmarkedPicturesList.getAdapter();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,20 @@ package fr.free.nrw.commons.campaigns;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import org.wikipedia.util.DateUtil;
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
|
|
@ -31,6 +29,7 @@ import fr.free.nrw.commons.utils.ViewUtil;
|
|||
*/
|
||||
public class CampaignView extends SwipableCardView {
|
||||
Campaign campaign;
|
||||
private LayoutCampaginBinding binding;
|
||||
private ViewHolder viewHolder;
|
||||
|
||||
public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView";
|
||||
|
|
@ -69,15 +68,15 @@ public class CampaignView extends SwipableCardView {
|
|||
@Override public boolean onSwipe(final View view) {
|
||||
view.setVisibility(View.GONE);
|
||||
((BaseActivity) getContext()).defaultKvStore
|
||||
.putBoolean(campaignPreference, false);
|
||||
.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false);
|
||||
ViewUtil.showLongToast(getContext(),
|
||||
getResources().getString(R.string.nearby_campaign_dismiss_message));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
final View rootView = inflate(getContext(), R.layout.layout_campagin, this);
|
||||
viewHolder = new ViewHolder(rootView);
|
||||
binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true);
|
||||
viewHolder = new ViewHolder();
|
||||
setOnClickListener(view -> {
|
||||
if (campaign != null) {
|
||||
if (campaign.isWLMCampaign()) {
|
||||
|
|
@ -90,27 +89,16 @@ public class CampaignView extends SwipableCardView {
|
|||
}
|
||||
|
||||
public class ViewHolder {
|
||||
|
||||
@BindView(R.id.iv_campaign)
|
||||
ImageView ivCampaign;
|
||||
@BindView(R.id.tv_title) TextView tvTitle;
|
||||
@BindView(R.id.tv_description) TextView tvDescription;
|
||||
@BindView(R.id.tv_dates) TextView tvDates;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
ButterKnife.bind(this, itemView);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (campaign != null) {
|
||||
ivCampaign.setImageDrawable(
|
||||
binding.ivCampaign.setImageDrawable(
|
||||
getResources().getDrawable(R.drawable.ic_campaign));
|
||||
|
||||
tvTitle.setText(campaign.getTitle());
|
||||
tvDescription.setText(campaign.getDescription());
|
||||
binding.tvTitle.setText(campaign.getTitle());
|
||||
binding.tvDescription.setText(campaign.getDescription());
|
||||
try {
|
||||
if (campaign.isWLMCampaign()) {
|
||||
tvDates.setText(
|
||||
binding.tvDates.setText(
|
||||
String.format("%1s - %2s", campaign.getStartDate(),
|
||||
campaign.getEndDate()));
|
||||
} else {
|
||||
|
|
@ -118,7 +106,7 @@ public class CampaignView extends SwipableCardView {
|
|||
.parse(campaign.getStartDate());
|
||||
final Date endDate = CommonsDateUtil.getIso8601DateFormatShort()
|
||||
.parse(campaign.getEndDate());
|
||||
tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
|
||||
binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
|
||||
DateUtil.getExtraShortDateString(endDate)));
|
||||
}
|
||||
} catch (final ParseException e) {
|
||||
|
|
|
|||
|
|
@ -27,30 +27,42 @@ class CategoriesModel @Inject constructor(
|
|||
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Returns if the item contains an year
|
||||
* @param item
|
||||
* Returns true if an item is considered to be a spammy category which should be ignored
|
||||
*
|
||||
* @param item a category item that needs to be validated to know if it is spammy or not
|
||||
* @return
|
||||
*/
|
||||
fun containsYear(item: String): Boolean {
|
||||
fun isSpammyCategory(item: String): Boolean {
|
||||
//Check for current and previous year to exclude these categories from removal
|
||||
val now = Calendar.getInstance()
|
||||
val year = now[Calendar.YEAR]
|
||||
val yearInString = year.toString()
|
||||
val prevYear = year - 1
|
||||
val curYear = now[Calendar.YEAR]
|
||||
val curYearInString = curYear.toString()
|
||||
val prevYear = curYear - 1
|
||||
val prevYearInString = prevYear.toString()
|
||||
Timber.d("Previous year: %s", prevYearInString)
|
||||
|
||||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||
//And that item does not equal the current year or previous year
|
||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
||||
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
||||
&& !item.contains(yearInString)
|
||||
&& !item.contains(prevYearInString)
|
||||
|| item.matches("(.*)needing(.*)".toRegex())
|
||||
|| item.matches("(.*)taken on(.*)".toRegex())
|
||||
|| item.matches(".*0s.*".toRegex())
|
||||
&& !item.matches(".*(200|201)0s.*".toRegex())
|
||||
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
||||
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
||||
val spammyCategory = item.matches("(.*)needing(.*)".toRegex())
|
||||
|| item.matches("(.*)taken on(.*)".toRegex())
|
||||
|
||||
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||
if (spammyCategory) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (mentionsDecade) {
|
||||
// Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029
|
||||
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
||||
return !recentDecade
|
||||
} else {
|
||||
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
||||
// anywhere within the string (.* is wildcard) (Issue #47)
|
||||
// And that item does not equal the current year or previous year
|
||||
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
||||
&& !item.contains(curYearInString)
|
||||
&& !item.contains(prevYearInString)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,7 +148,11 @@ class CategoriesModel @Inject constructor(
|
|||
return Observable.fromIterable(categoryNames)
|
||||
.map { categoryName ->
|
||||
buildCategories(categoryName)
|
||||
}.toList().toObservable()
|
||||
}
|
||||
.filter { categoryItem ->
|
||||
categoryItem.name != "Hidden"
|
||||
}
|
||||
.toList().toObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,12 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding;
|
||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
|
||||
|
|
@ -29,7 +28,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.wikipedia.page.PageTitle;
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
|
||||
|
||||
/**
|
||||
* This activity displays details of a particular category
|
||||
|
|
@ -45,23 +44,23 @@ public class CategoryDetailsActivity extends BaseActivity
|
|||
private CategoriesMediaFragment categoriesMediaFragment;
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private String categoryName;
|
||||
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
|
||||
@BindView(R.id.tab_layout) TabLayout tabLayout;
|
||||
@BindView(R.id.viewPager) ViewPager viewPager;
|
||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
|
||||
private ActivityCategoryDetailsBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_category_details);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
binding = ActivityCategoryDetailsBinding.inflate(getLayoutInflater());
|
||||
final View view = binding.getRoot();
|
||||
setContentView(view);
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(2);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
setSupportActionBar(toolbar);
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.viewPager.setOffscreenPageLimit(2);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setTabs();
|
||||
setPageTitle();
|
||||
|
|
@ -110,12 +109,12 @@ public class CategoryDetailsActivity extends BaseActivity
|
|||
*/
|
||||
@Override
|
||||
public void onMediaClicked(int position) {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
viewPager.setVisibility(View.GONE);
|
||||
mediaContainer.setVisibility(View.VISIBLE);
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.GONE);
|
||||
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
|
|
@ -216,9 +215,9 @@ public class CategoryDetailsActivity extends BaseActivity
|
|||
@Override
|
||||
public void onBackPressed() {
|
||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
viewPager.setVisibility(View.VISIBLE);
|
||||
mediaContainer.setVisibility(View.GONE);
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.viewPager.setVisibility(View.VISIBLE);
|
||||
binding.mediaContainer.setVisibility(View.GONE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_E
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import java.util.Map;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
import retrofit2.http.QueryMap;
|
||||
|
||||
/**
|
||||
* Interface for interacting with Commons category related APIs
|
||||
*/
|
||||
public interface CategoryInterface {
|
||||
|
||||
/**
|
||||
* Searches for categories with the specified name.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70"
|
||||
+ "&gsrnamespace=14")
|
||||
Single<MwQueryResponse> searchCategories(@Query("gsrsearch") String filter,
|
||||
@Query("gsrlimit") int itemLimit,
|
||||
@Query("gsroffset") int offset);
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified prefix.
|
||||
*
|
||||
* @param prefix The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail"
|
||||
+ "&pithumbsize=70")
|
||||
Single<MwQueryResponse> searchCategoriesForPrefix(@Query("gacprefix") String prefix,
|
||||
@Query("gaclimit") int itemLimit,
|
||||
@Query("gacoffset") int offset);
|
||||
|
||||
/**
|
||||
* Fetches categories starting and ending with a specified name.
|
||||
*
|
||||
* @param startingCategory Name of the category to start
|
||||
* @param endingCategory Name of the category to end
|
||||
* @param itemLimit How many categories to return
|
||||
* @param offset offset
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail"
|
||||
+ "&pithumbsize=70")
|
||||
Single<MwQueryResponse> getCategoriesByName(@Query("gacfrom") String startingCategory,
|
||||
@Query("gacto") String endingCategory,
|
||||
@Query("gaclimit") int itemLimit,
|
||||
@Query("gacoffset") int offset);
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=categorymembers&gcmtype=subcat"
|
||||
+ "&prop=info&gcmlimit=50")
|
||||
Single<MwQueryResponse> getSubCategoryList(@Query("gcmtitle") String categoryName,
|
||||
@QueryMap(encoded = true) Map<String, String> continuation);
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=categories&prop=info&gcllimit=50")
|
||||
Single<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName,
|
||||
@QueryMap(encoded = true) Map<String, String> continuation);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.Single
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.QueryMap
|
||||
|
||||
/**
|
||||
* Interface for interacting with Commons category related APIs
|
||||
*/
|
||||
interface CategoryInterface {
|
||||
/**
|
||||
* Searches for categories with the specified name.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14")
|
||||
fun searchCategories(
|
||||
@Query("gsrsearch") filter: String?,
|
||||
@Query("gsrlimit") itemLimit: Int,
|
||||
@Query("gsroffset") offset: Int
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified prefix.
|
||||
*
|
||||
* @param prefix The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||
fun searchCategoriesForPrefix(
|
||||
@Query("gacprefix") prefix: String?,
|
||||
@Query("gaclimit") itemLimit: Int,
|
||||
@Query("gacoffset") offset: Int
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
/**
|
||||
* Fetches categories starting and ending with a specified name.
|
||||
*
|
||||
* @param startingCategory Name of the category to start
|
||||
* @param endingCategory Name of the category to end
|
||||
* @param itemLimit How many categories to return
|
||||
* @param offset offset
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||
fun getCategoriesByName(
|
||||
@Query("gacfrom") startingCategory: String?,
|
||||
@Query("gacto") endingCategory: String?,
|
||||
@Query("gaclimit") itemLimit: Int,
|
||||
@Query("gacoffset") offset: Int
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||
fun getSubCategoryList(
|
||||
@Query("gcmtitle") categoryName: String,
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
|
||||
fun getParentCategoryList(
|
||||
@Query("titles") categoryName: String?,
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||
): Single<MwQueryResponse>
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class CategoryItem(val name: String, val description: String?,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.os.Parcelable
|
|||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.upload.UploadItem
|
||||
|
|
@ -12,8 +13,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail
|
|||
import fr.free.nrw.commons.upload.WikidataPlace
|
||||
import fr.free.nrw.commons.upload.WikidataPlace.Companion.from
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
@Entity(tableName = "contribution")
|
||||
@Parcelize
|
||||
|
|
@ -43,7 +45,11 @@ data class Contribution constructor(
|
|||
var hasInvalidLocation : Int = 0,
|
||||
var contentUri: Uri? = null,
|
||||
var countryCode : String? = null,
|
||||
var imageSHA1 : String? = null
|
||||
var imageSHA1 : String? = null,
|
||||
/**
|
||||
* Number of times a contribution has been retried after a failure
|
||||
*/
|
||||
var retries: Int = 0
|
||||
) : Parcelable {
|
||||
|
||||
fun completeWith(media: Media): Contribution {
|
||||
|
|
@ -111,6 +117,21 @@ data class Contribution constructor(
|
|||
*/
|
||||
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
||||
descriptions.filter { it.descriptionText.isNotEmpty() }
|
||||
.joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
||||
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
||||
}
|
||||
|
||||
val fileKey : String? get() = chunkInfo?.uploadResult?.filekey
|
||||
val localUriPath: File? get() = localUri?.path?.let { File(it) }
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
||||
}
|
||||
|
||||
fun isPaused(): Boolean {
|
||||
return CommonsApplication.pauseUploads[pageId] ?: false
|
||||
}
|
||||
|
||||
fun unpause() {
|
||||
CommonsApplication.pauseUploads[pageId] = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ package fr.free.nrw.commons.contributions;
|
|||
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.Manifest.permission;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.filepicker.DefaultCallback;
|
||||
|
|
@ -16,8 +15,13 @@ import fr.free.nrw.commons.filepicker.FilePicker;
|
|||
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -31,6 +35,13 @@ public class ContributionController {
|
|||
|
||||
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
|
||||
private final JsonKvStore defaultKvStore;
|
||||
private LatLng locationBeforeImageCapture;
|
||||
private boolean isInAppCameraUpload;
|
||||
public LocationPermissionCallback locationPermissionCallback;
|
||||
private LocationPermissionsHelper locationPermissionsHelper;
|
||||
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
|
||||
@Inject
|
||||
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
|
||||
|
|
@ -40,7 +51,8 @@ public class ContributionController {
|
|||
/**
|
||||
* Check for permissions and initiate camera click
|
||||
*/
|
||||
public void initiateCameraPick(Activity activity) {
|
||||
public void initiateCameraPick(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
||||
if (!useExtStorage) {
|
||||
initiateCameraUpload(activity);
|
||||
|
|
@ -48,10 +60,133 @@ public class ContributionController {
|
|||
}
|
||||
|
||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
() -> initiateCameraUpload(activity),
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale);
|
||||
() -> {
|
||||
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
|
||||
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
|
||||
askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher);
|
||||
} else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
} else {
|
||||
initiateCameraUpload(activity);
|
||||
}
|
||||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks users to provide location access
|
||||
*
|
||||
* @param activity
|
||||
*/
|
||||
private void createDialogsAndHandleLocationPermissions(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
locationPermissionCallback = new LocationPermissionCallback() {
|
||||
@Override
|
||||
public void onLocationPermissionDenied(String toastMessage) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
toastMessage,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
initiateCameraUpload(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationPermissionGranted() {
|
||||
if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
showLocationOffDialog(activity, R.string.in_app_camera_needs_location,
|
||||
R.string.in_app_camera_location_unavailable);
|
||||
} else {
|
||||
initiateCameraUpload(activity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
locationPermissionsHelper = new LocationPermissionsHelper(
|
||||
activity, locationManager, locationPermissionCallback);
|
||||
if (inAppCameraLocationPermissionLauncher != null) {
|
||||
inAppCameraLocationPermissionLauncher.launch(
|
||||
new String[]{permission.ACCESS_FINE_LOCATION});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog alerting the user about location services being off
|
||||
* and asking them to turn it on
|
||||
* TODO: Add a seperate callback in LocationPermissionsHelper for this.
|
||||
* Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
|
||||
*
|
||||
* @param activity Activity reference
|
||||
* @param dialogTextResource Resource id of text to be shown in dialog
|
||||
* @param toastTextResource Resource id of text to be shown in toast
|
||||
*/
|
||||
private void showLocationOffDialog(Activity activity, int dialogTextResource,
|
||||
int toastTextResource) {
|
||||
DialogUtil
|
||||
.showAlertDialog(activity,
|
||||
activity.getString(R.string.ask_to_turn_location_on),
|
||||
activity.getString(dialogTextResource),
|
||||
activity.getString(R.string.title_app_shortcut_setting),
|
||||
activity.getString(R.string.cancel),
|
||||
() -> locationPermissionsHelper.openLocationSettings(activity),
|
||||
() -> {
|
||||
Toast.makeText(activity, activity.getString(toastTextResource),
|
||||
Toast.LENGTH_LONG).show();
|
||||
initiateCameraUpload(activity);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void handleShowRationaleFlowCameraLocation(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
|
||||
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
||||
activity.getString(android.R.string.ok),
|
||||
activity.getString(android.R.string.cancel),
|
||||
() -> {
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
},
|
||||
() -> locationPermissionCallback.onLocationPermissionDenied(
|
||||
activity.getString(R.string.in_app_camera_location_permission_denied)),
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest user to attach location information with pictures. If the user selects "Yes", then:
|
||||
* <p>
|
||||
* Location is taken from the EXIF if the default camera application does not redact location
|
||||
* tags.
|
||||
* <p>
|
||||
* Otherwise, if the EXIF metadata does not have location information, then location captured by
|
||||
* the app is used
|
||||
*
|
||||
* @param activity
|
||||
*/
|
||||
private void askUserToAllowLocationAccess(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
DialogUtil.showAlertDialog(activity,
|
||||
activity.getString(R.string.in_app_camera_location_permission_title),
|
||||
activity.getString(R.string.in_app_camera_location_access_explanation),
|
||||
activity.getString(R.string.option_allow),
|
||||
activity.getString(R.string.option_dismiss),
|
||||
() -> {
|
||||
defaultKvStore.putBoolean("inAppCameraLocationPref", true);
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
},
|
||||
() -> {
|
||||
ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied);
|
||||
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
|
||||
initiateCameraUpload(activity);
|
||||
},
|
||||
null,
|
||||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,44 +200,36 @@ public class ContributionController {
|
|||
* Initiate gallery picker with permission
|
||||
*/
|
||||
public void initiateCustomGalleryPickWithPermission(final Activity activity) {
|
||||
setPickerConfiguration(activity,true);
|
||||
setPickerConfiguration(activity, true);
|
||||
|
||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
() -> {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||
PermissionUtils.checkPermissionsAndPerformAction(
|
||||
activity,
|
||||
permission.ACCESS_MEDIA_LOCATION,
|
||||
() -> {},
|
||||
R.string.media_location_permission_denied,
|
||||
R.string.add_location_manually
|
||||
);
|
||||
}
|
||||
FilePicker.openCustomSelector(activity, 0);
|
||||
},
|
||||
() -> FilePicker.openCustomSelector(activity, 0),
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale);
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open chooser for gallery uploads
|
||||
*/
|
||||
private void initiateGalleryUpload(final Activity activity, final boolean allowMultipleUploads) {
|
||||
private void initiateGalleryUpload(final Activity activity,
|
||||
final boolean allowMultipleUploads) {
|
||||
setPickerConfiguration(activity, allowMultipleUploads);
|
||||
FilePicker.openGallery(activity, 0);
|
||||
boolean openDocumentIntentPreferred = defaultKvStore.getBoolean(
|
||||
"openDocumentPhotoPickerPref", true);
|
||||
FilePicker.openGallery(activity, 0, openDocumentIntentPreferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets configuration for file picker
|
||||
*/
|
||||
private void setPickerConfiguration(Activity activity,
|
||||
boolean allowMultipleUploads) {
|
||||
boolean allowMultipleUploads) {
|
||||
boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
||||
FilePicker.configuration(activity)
|
||||
.setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
|
||||
.setAllowMultiplePickInGallery(allowMultipleUploads);
|
||||
.setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
|
||||
.setAllowMultiplePickInGallery(allowMultipleUploads);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,42 +237,50 @@ public class ContributionController {
|
|||
*/
|
||||
private void initiateCameraUpload(Activity activity) {
|
||||
setPickerConfiguration(activity, false);
|
||||
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
|
||||
locationBeforeImageCapture = locationManager.getLastLocation();
|
||||
}
|
||||
isInAppCameraUpload = true;
|
||||
FilePicker.openCameraForImage(activity, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches callback for file picker.
|
||||
*/
|
||||
public void handleActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
FilePicker.handleActivityResult(requestCode, resultCode, data, activity, new DefaultCallback() {
|
||||
public void handleActivityResult(Activity activity, int requestCode, int resultCode,
|
||||
Intent data) {
|
||||
FilePicker.handleActivityResult(requestCode, resultCode, data, activity,
|
||||
new DefaultCallback() {
|
||||
|
||||
@Override
|
||||
public void onCanceled(final ImageSource source, final int type) {
|
||||
super.onCanceled(source, type);
|
||||
defaultKvStore.remove(PLACE_OBJECT);
|
||||
}
|
||||
@Override
|
||||
public void onCanceled(final ImageSource source, final int type) {
|
||||
super.onCanceled(source, type);
|
||||
defaultKvStore.remove(PLACE_OBJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) {
|
||||
ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images);
|
||||
}
|
||||
@Override
|
||||
public void onImagePickerError(Exception e, FilePicker.ImageSource source,
|
||||
int type) {
|
||||
ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) {
|
||||
Intent intent = handleImagesPicked(activity, imagesFiles);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles,
|
||||
FilePicker.ImageSource source, int type) {
|
||||
Intent intent = handleImagesPicked(activity, imagesFiles);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public List<UploadableFile> handleExternalImagesPicked(Activity activity,
|
||||
Intent data) {
|
||||
Intent data) {
|
||||
return FilePicker.handleExternalImagesPicked(data, activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns intent to be passed to upload activity
|
||||
* Attaches place object for nearby uploads
|
||||
* Returns intent to be passed to upload activity Attaches place object for nearby uploads and
|
||||
* location before image capture if in-app camera is used
|
||||
*/
|
||||
private Intent handleImagesPicked(Context context,
|
||||
List<UploadableFile> imagesFiles) {
|
||||
|
|
@ -159,7 +294,17 @@ public class ContributionController {
|
|||
shareIntent.putExtra(PLACE_OBJECT, place);
|
||||
}
|
||||
|
||||
if (locationBeforeImageCapture != null) {
|
||||
shareIntent.putExtra(
|
||||
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
|
||||
locationBeforeImageCapture);
|
||||
}
|
||||
|
||||
shareIntent.putExtra(
|
||||
UploadActivity.IN_APP_CAMERA_UPLOAD,
|
||||
isInAppCameraUpload
|
||||
);
|
||||
isInAppCameraUpload = false; // reset the flag for next use
|
||||
return shareIntent;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.room.Update;
|
|||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@ import androidx.annotation.Nullable;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog.Builder;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
||||
import fr.free.nrw.commons.databinding.LayoutContributionBinding;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
|
@ -29,29 +27,8 @@ import java.io.File;
|
|||
public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Callback callback;
|
||||
@BindView(R.id.contributionImage)
|
||||
SimpleDraweeView imageView;
|
||||
@BindView(R.id.contributionTitle)
|
||||
TextView titleView;
|
||||
@BindView(R.id.authorView)
|
||||
TextView authorView;
|
||||
@BindView(R.id.contributionState)
|
||||
TextView stateView;
|
||||
@BindView(R.id.contributionSequenceNumber)
|
||||
TextView seqNumView;
|
||||
@BindView(R.id.contributionProgress)
|
||||
ProgressBar progressView;
|
||||
@BindView(R.id.image_options)
|
||||
RelativeLayout imageOptions;
|
||||
@BindView(R.id.wikipediaButton)
|
||||
ImageButton addToWikipediaButton;
|
||||
@BindView(R.id.retryButton)
|
||||
ImageButton retryButton;
|
||||
@BindView(R.id.cancelButton)
|
||||
ImageButton cancelButton;
|
||||
@BindView(R.id.pauseResumeButton)
|
||||
ImageButton pauseResumeButton;
|
||||
|
||||
LayoutContributionBinding binding;
|
||||
|
||||
private int position;
|
||||
private Contribution contribution;
|
||||
|
|
@ -67,9 +44,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
super(parent);
|
||||
this.parent = parent;
|
||||
this.mediaClient = mediaClient;
|
||||
ButterKnife.bind(this, parent);
|
||||
this.callback = callback;
|
||||
|
||||
binding = LayoutContributionBinding.bind(parent);
|
||||
|
||||
binding.retryButton.setOnClickListener(v -> retryUpload());
|
||||
binding.cancelButton.setOnClickListener(v -> deleteUpload());
|
||||
binding.contributionImage.setOnClickListener(v -> imageClicked());
|
||||
binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
|
||||
binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked());
|
||||
|
||||
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
|
||||
an upload might take a dozen seconds. */
|
||||
AlertDialog.Builder builder = new Builder(parent.getContext());
|
||||
|
|
@ -87,14 +71,17 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
this.contribution = contribution;
|
||||
this.position = position;
|
||||
titleView.setText(contribution.getMedia().getMostRelevantCaption());
|
||||
authorView.setText(contribution.getMedia().getAuthor());
|
||||
binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption());
|
||||
binding.authorView.setText(contribution.getMedia().getAuthor());
|
||||
|
||||
//Removes flicker of loading image.
|
||||
imageView.getHierarchy().setFadeDuration(0);
|
||||
binding.contributionImage.getHierarchy().setFadeDuration(0);
|
||||
|
||||
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
|
||||
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
|
||||
binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
|
||||
binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
|
||||
|
||||
|
||||
|
||||
|
||||
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
|
||||
contribution.getLocalUri());
|
||||
|
|
@ -103,73 +90,77 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
||||
.setProgressiveRenderingEnabled(true)
|
||||
.build();
|
||||
} else if(imageSource != null) {
|
||||
}
|
||||
else if (URLUtil.isFileUrl(imageSource)){
|
||||
imageRequest=ImageRequest.fromUri(Uri.parse(imageSource));
|
||||
}
|
||||
else if(imageSource != null) {
|
||||
final File file = new File(imageSource);
|
||||
imageRequest = ImageRequest.fromFile(file);
|
||||
}
|
||||
|
||||
if(imageRequest != null){
|
||||
imageView.setImageRequest(imageRequest);
|
||||
binding.contributionImage.setImageRequest(imageRequest);
|
||||
}
|
||||
}
|
||||
|
||||
seqNumView.setText(String.valueOf(position + 1));
|
||||
seqNumView.setVisibility(View.VISIBLE);
|
||||
binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
|
||||
binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
|
||||
|
||||
addToWikipediaButton.setVisibility(View.GONE);
|
||||
binding.wikipediaButton.setVisibility(View.GONE);
|
||||
switch (contribution.getState()) {
|
||||
case Contribution.STATE_COMPLETED:
|
||||
stateView.setVisibility(View.GONE);
|
||||
progressView.setVisibility(View.GONE);
|
||||
imageOptions.setVisibility(View.GONE);
|
||||
stateView.setText("");
|
||||
binding.contributionState.setVisibility(View.GONE);
|
||||
binding.contributionProgress.setVisibility(View.GONE);
|
||||
binding.imageOptions.setVisibility(View.GONE);
|
||||
binding.contributionState.setText("");
|
||||
checkIfMediaExistsOnWikipediaPage(contribution);
|
||||
break;
|
||||
case Contribution.STATE_QUEUED:
|
||||
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
|
||||
progressView.setVisibility(View.GONE);
|
||||
stateView.setVisibility(View.VISIBLE);
|
||||
stateView.setText(R.string.contribution_state_queued);
|
||||
imageOptions.setVisibility(View.GONE);
|
||||
binding.contributionProgress.setVisibility(View.GONE);
|
||||
binding.contributionState.setVisibility(View.VISIBLE);
|
||||
binding.contributionState.setText(R.string.contribution_state_queued);
|
||||
binding.imageOptions.setVisibility(View.GONE);
|
||||
break;
|
||||
case Contribution.STATE_IN_PROGRESS:
|
||||
stateView.setVisibility(View.GONE);
|
||||
progressView.setVisibility(View.VISIBLE);
|
||||
addToWikipediaButton.setVisibility(View.GONE);
|
||||
pauseResumeButton.setVisibility(View.VISIBLE);
|
||||
cancelButton.setVisibility(View.GONE);
|
||||
retryButton.setVisibility(View.GONE);
|
||||
imageOptions.setVisibility(View.VISIBLE);
|
||||
binding.contributionState.setVisibility(View.GONE);
|
||||
binding.contributionProgress.setVisibility(View.VISIBLE);
|
||||
binding.wikipediaButton.setVisibility(View.GONE);
|
||||
binding.pauseResumeButton.setVisibility(View.VISIBLE);
|
||||
binding.cancelButton.setVisibility(View.GONE);
|
||||
binding.retryButton.setVisibility(View.GONE);
|
||||
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||
final long total = contribution.getDataLength();
|
||||
final long transferred = contribution.getTransferred();
|
||||
if (transferred == 0 || transferred >= total) {
|
||||
progressView.setIndeterminate(true);
|
||||
binding.contributionProgress.setIndeterminate(true);
|
||||
} else {
|
||||
progressView.setIndeterminate(false);
|
||||
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
|
||||
binding.contributionProgress.setIndeterminate(false);
|
||||
binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100));
|
||||
}
|
||||
break;
|
||||
case Contribution.STATE_PAUSED:
|
||||
progressView.setVisibility(View.GONE);
|
||||
stateView.setVisibility(View.VISIBLE);
|
||||
stateView.setText(R.string.paused);
|
||||
cancelButton.setVisibility(View.VISIBLE);
|
||||
retryButton.setVisibility(View.GONE);
|
||||
pauseResumeButton.setVisibility(View.VISIBLE);
|
||||
imageOptions.setVisibility(View.VISIBLE);
|
||||
binding.contributionProgress.setVisibility(View.GONE);
|
||||
binding.contributionState.setVisibility(View.VISIBLE);
|
||||
binding.contributionState.setText(R.string.paused);
|
||||
binding.cancelButton.setVisibility(View.VISIBLE);
|
||||
binding.retryButton.setVisibility(View.GONE);
|
||||
binding.pauseResumeButton.setVisibility(View.VISIBLE);
|
||||
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||
setResume();
|
||||
if(pausingPopUp.isShowing()){
|
||||
pausingPopUp.hide();
|
||||
}
|
||||
break;
|
||||
case Contribution.STATE_FAILED:
|
||||
stateView.setVisibility(View.VISIBLE);
|
||||
stateView.setText(R.string.contribution_state_failed);
|
||||
progressView.setVisibility(View.GONE);
|
||||
cancelButton.setVisibility(View.VISIBLE);
|
||||
retryButton.setVisibility(View.VISIBLE);
|
||||
pauseResumeButton.setVisibility(View.GONE);
|
||||
imageOptions.setVisibility(View.VISIBLE);
|
||||
binding.contributionState.setVisibility(View.VISIBLE);
|
||||
binding.contributionState.setText(R.string.contribution_state_failed);
|
||||
binding.contributionProgress.setVisibility(View.GONE);
|
||||
binding.cancelButton.setVisibility(View.VISIBLE);
|
||||
binding.retryButton.setVisibility(View.VISIBLE);
|
||||
binding.pauseResumeButton.setVisibility(View.GONE);
|
||||
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -203,11 +194,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
*/
|
||||
private void displayWikipediaButton(Boolean mediaExists) {
|
||||
if (!mediaExists) {
|
||||
addToWikipediaButton.setVisibility(View.VISIBLE);
|
||||
binding.wikipediaButton.setVisibility(View.VISIBLE);
|
||||
isWikipediaButtonDisplayed = true;
|
||||
cancelButton.setVisibility(View.GONE);
|
||||
retryButton.setVisibility(View.GONE);
|
||||
imageOptions.setVisibility(View.VISIBLE);
|
||||
binding.cancelButton.setVisibility(View.GONE);
|
||||
binding.retryButton.setVisibility(View.GONE);
|
||||
binding.imageOptions.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +220,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
/**
|
||||
* Retry upload when it is failed
|
||||
*/
|
||||
@OnClick(R.id.retryButton)
|
||||
public void retryUpload() {
|
||||
callback.retryUpload(contribution);
|
||||
}
|
||||
|
|
@ -237,17 +227,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
/**
|
||||
* Delete a failed upload attempt
|
||||
*/
|
||||
@OnClick(R.id.cancelButton)
|
||||
public void deleteUpload() {
|
||||
callback.deleteUpload(contribution);
|
||||
}
|
||||
|
||||
@OnClick(R.id.contributionImage)
|
||||
public void imageClicked() {
|
||||
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
|
||||
}
|
||||
|
||||
@OnClick(R.id.wikipediaButton)
|
||||
public void wikipediaButtonClicked() {
|
||||
callback.addImageToWikipedia(contribution);
|
||||
}
|
||||
|
|
@ -255,9 +242,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
/**
|
||||
* Triggers a callback for pause/resume
|
||||
*/
|
||||
@OnClick(R.id.pauseResumeButton)
|
||||
public void onPauseResumeButtonClicked() {
|
||||
if (pauseResumeButton.getTag().toString().equals("pause")) {
|
||||
if (binding.pauseResumeButton.getTag().toString().equals("pause")) {
|
||||
pause();
|
||||
} else {
|
||||
resume();
|
||||
|
|
@ -279,16 +265,16 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
* Update pause/resume button to show pause state
|
||||
*/
|
||||
private void setPaused() {
|
||||
pauseResumeButton.setImageResource(R.drawable.pause_icon);
|
||||
pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
|
||||
binding.pauseResumeButton.setImageResource(R.drawable.pause_icon);
|
||||
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pause/resume button to show resume state
|
||||
*/
|
||||
private void setResume() {
|
||||
pauseResumeButton.setImageResource(R.drawable.play_icon);
|
||||
pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
|
||||
binding.pauseResumeButton.setImageResource(R.drawable.play_icon);
|
||||
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
|
||||
}
|
||||
|
||||
public ImageRequest getImageRequest() {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import static android.content.Context.SENSOR_SERVICE;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
||||
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||
|
||||
import android.Manifest;
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
|
|
@ -21,6 +28,9 @@ import android.widget.CheckBox;
|
|||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
|
@ -29,17 +39,17 @@ import androidx.fragment.app.FragmentTransaction;
|
|||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||
import fr.free.nrw.commons.notification.models.Notification;
|
||||
import fr.free.nrw.commons.notification.NotificationController;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import androidx.work.WorkManager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
|
|
@ -73,18 +83,27 @@ import io.reactivex.schedulers.Schedulers;
|
|||
import timber.log.Timber;
|
||||
|
||||
public class ContributionsFragment
|
||||
extends CommonsDaggerSupportFragment
|
||||
implements
|
||||
OnBackStackChangedListener,
|
||||
LocationUpdateListener,
|
||||
extends CommonsDaggerSupportFragment
|
||||
implements
|
||||
OnBackStackChangedListener,
|
||||
LocationUpdateListener,
|
||||
MediaDetailProvider,
|
||||
ICampaignsView, ContributionsContract.View, Callback{
|
||||
@Inject @Named("default_preferences") JsonKvStore store;
|
||||
@Inject NearbyController nearbyController;
|
||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
@Inject CampaignsPresenter presenter;
|
||||
@Inject LocationServiceManager locationManager;
|
||||
@Inject NotificationController notificationController;
|
||||
SensorEventListener,
|
||||
ICampaignsView, ContributionsContract.View, Callback {
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore store;
|
||||
@Inject
|
||||
NearbyController nearbyController;
|
||||
@Inject
|
||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
@Inject
|
||||
CampaignsPresenter presenter;
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
@Inject
|
||||
NotificationController notificationController;
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
|
|
@ -92,20 +111,18 @@ public class ContributionsFragment
|
|||
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
|
||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
||||
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
|
||||
private static final int MAX_RETRIES = 10;
|
||||
|
||||
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
|
||||
@BindView(R.id.campaigns_view) CampaignView campaignView;
|
||||
@BindView(R.id.limited_connection_enabled_layout) LinearLayout limitedConnectionEnabledLayout;
|
||||
@BindView(R.id.limited_connection_description_text_view) TextView limitedConnectionDescriptionTextView;
|
||||
|
||||
public FragmentContributionsBinding binding;
|
||||
|
||||
@Inject ContributionsPresenter contributionsPresenter;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private LatLng curLatLng;
|
||||
private LatLng currentLatLng;
|
||||
|
||||
private boolean firstLocationUpdate = true;
|
||||
private boolean isFragmentAttachedBefore = false;
|
||||
private View checkBoxView;
|
||||
private CheckBox checkBox;
|
||||
|
|
@ -117,6 +134,34 @@ public class ContributionsFragment
|
|||
String userName;
|
||||
private boolean isUserProfile;
|
||||
|
||||
private SensorManager mSensorManager;
|
||||
private Sensor mLight;
|
||||
private float direction;
|
||||
private ActivityResultLauncher<String[]> nearbyLocationPermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||
new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
@Override
|
||||
public void onActivityResult(Map<String, Boolean> result) {
|
||||
boolean areAllGranted = true;
|
||||
for (final boolean b : result.values()) {
|
||||
areAllGranted = areAllGranted && b;
|
||||
}
|
||||
|
||||
if (areAllGranted) {
|
||||
onLocationPermissionGranted();
|
||||
} else {
|
||||
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
||||
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
|
||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
||||
} else {
|
||||
displayYouWontSeeNearbyMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@NonNull
|
||||
public static ContributionsFragment newInstance() {
|
||||
ContributionsFragment fragment = new ContributionsFragment();
|
||||
|
|
@ -133,17 +178,21 @@ public class ContributionsFragment
|
|||
userName = getArguments().getString(KEY_USERNAME);
|
||||
isUserProfile = true;
|
||||
}
|
||||
mSensorManager = (SensorManager) getActivity().getSystemService(SENSOR_SERVICE);
|
||||
mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
binding = FragmentContributionsBinding.inflate(inflater, container, false);
|
||||
|
||||
initWLMCampaign();
|
||||
presenter.onAttachView(this);
|
||||
contributionsPresenter.onAttachView(this);
|
||||
campaignView.setVisibility(View.GONE);
|
||||
binding.campaignsView.setVisibility(View.GONE);
|
||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
|
|
@ -153,6 +202,7 @@ public class ContributionsFragment
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
|
||||
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
|
||||
|
|
@ -163,13 +213,13 @@ public class ContributionsFragment
|
|||
|
||||
initFragments();
|
||||
if(isUserProfile) {
|
||||
limitedConnectionEnabledLayout.setVisibility(View.GONE);
|
||||
binding.limitedConnectionEnabledLayout.setVisibility(View.GONE);
|
||||
}else {
|
||||
upDateUploadCount();
|
||||
}
|
||||
if(shouldShowMediaDetailsFragment){
|
||||
if (shouldShowMediaDetailsFragment) {
|
||||
showMediaDetailPagerFragment();
|
||||
}else{
|
||||
} else {
|
||||
if (mediaDetailPagerFragment != null) {
|
||||
removeFragment(mediaDetailPagerFragment);
|
||||
}
|
||||
|
|
@ -180,9 +230,9 @@ public class ContributionsFragment
|
|||
&& sessionManager.getCurrentAccount() != null && !isUserProfile) {
|
||||
setUploadCount();
|
||||
}
|
||||
limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener);
|
||||
binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener);
|
||||
setHasOptionsMenu(true);
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -195,10 +245,13 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
|
||||
// Removing contributions menu items for ProfileActivity
|
||||
if (getActivity() instanceof ProfileActivity) { return; }
|
||||
if (getActivity() instanceof ProfileActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.contribution_activity_notification_menu, menu);
|
||||
|
||||
|
|
@ -220,7 +273,7 @@ public class ContributionsFragment
|
|||
throwable -> Timber.e(throwable, "Error occurred while loading notifications")));
|
||||
}
|
||||
|
||||
public void scrollToTop( ){
|
||||
public void scrollToTop() {
|
||||
if (contributionsListFragment != null) {
|
||||
contributionsListFragment.scrollToTop();
|
||||
}
|
||||
|
|
@ -242,22 +295,17 @@ public class ContributionsFragment
|
|||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
|
||||
|
||||
checkable.setChecked(isEnabled);
|
||||
if (isEnabled) {
|
||||
limitedConnectionEnabledLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
limitedConnectionEnabledLayout.setVisibility(View.GONE);
|
||||
if (binding!=null) {
|
||||
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
|
||||
checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
((MainActivity) getActivity()).toggleLimitedConnectionMode();
|
||||
boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
|
||||
if (isEnabled) {
|
||||
limitedConnectionEnabledLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
limitedConnectionEnabledLayout.setVisibility(View.GONE);
|
||||
}
|
||||
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
|
||||
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -285,28 +333,31 @@ public class ContributionsFragment
|
|||
*/
|
||||
private void showContributionsListFragment() {
|
||||
// show nearby card view on contributions list is visible
|
||||
if (nearbyNotificationCardView != null && !isUserProfile) {
|
||||
if (binding.cardViewNearby != null && !isUserProfile) {
|
||||
if (store.getBoolean("displayNearbyCardView", true)) {
|
||||
if (nearbyNotificationCardView.cardViewVisibilityState
|
||||
if (binding.cardViewNearby.cardViewVisibilityState
|
||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
nearbyNotificationCardView.setVisibility(View.VISIBLE);
|
||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment);
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||
mediaDetailPagerFragment);
|
||||
}
|
||||
|
||||
private void showMediaDetailPagerFragment() {
|
||||
// hide nearby card view on media detail is visible
|
||||
setupViewForMediaDetails();
|
||||
showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, contributionsListFragment);
|
||||
showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG,
|
||||
contributionsListFragment);
|
||||
}
|
||||
|
||||
private void setupViewForMediaDetails() {
|
||||
campaignView.setVisibility(View.GONE);
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
if (binding!=null) {
|
||||
binding.campaignsView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -328,7 +379,8 @@ public class ContributionsFragment
|
|||
showContributionsListFragment();
|
||||
}
|
||||
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment);
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||
mediaDetailPagerFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -351,7 +403,7 @@ public class ContributionsFragment
|
|||
transaction.addToBackStack(tag);
|
||||
transaction.commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
}else if (!fragment.isAdded() && otherFragment != null ) {
|
||||
} else if (!fragment.isAdded() && otherFragment != null) {
|
||||
transaction.hide(otherFragment);
|
||||
transaction.add(R.id.root_frame, fragment, tag);
|
||||
transaction.addToBackStack(tag);
|
||||
|
|
@ -376,21 +428,21 @@ public class ContributionsFragment
|
|||
@SuppressWarnings("ConstantConditions")
|
||||
private void setUploadCount() {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::displayUploadCount,
|
||||
t -> Timber.e(t, "Fetching upload count failed")
|
||||
));
|
||||
.getUploadCount(((MainActivity) getActivity()).sessionManager.getCurrentAccount().name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::displayUploadCount,
|
||||
t -> Timber.e(t, "Fetching upload count failed")
|
||||
));
|
||||
}
|
||||
|
||||
private void displayUploadCount(Integer uploadCount) {
|
||||
if (getActivity().isFinishing()
|
||||
|| getResources() == null) {
|
||||
|| getResources() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
((MainActivity)getActivity()).setNumOfUploads(uploadCount);
|
||||
((MainActivity) getActivity()).setNumOfUploads(uploadCount);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -399,6 +451,7 @@ public class ContributionsFragment
|
|||
super.onPause();
|
||||
locationManager.removeLocationListener(this);
|
||||
locationManager.unregisterLocationManager();
|
||||
mSensorManager.unregisterListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -410,9 +463,13 @@ public class ContributionsFragment
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
contributionsPresenter.onAttachView(this);
|
||||
firstLocationUpdate = true;
|
||||
locationManager.addLocationListener(this);
|
||||
nearbyNotificationCardView.permissionRequestButton.setOnClickListener(v -> {
|
||||
|
||||
if (binding==null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.cardViewNearby.permissionRequestButton.setOnClickListener(v -> {
|
||||
showNearbyCardPermissionRationale();
|
||||
});
|
||||
|
||||
|
|
@ -420,13 +477,20 @@ public class ContributionsFragment
|
|||
if (mediaDetailPagerFragment == null && !isUserProfile) {
|
||||
if (store.getBoolean("displayNearbyCardView", true)) {
|
||||
checkPermissionsAndShowNearbyCardView();
|
||||
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
nearbyNotificationCardView.setVisibility(View.VISIBLE);
|
||||
|
||||
// Calling nearby card to keep showing it even when user clicks on it and comes back
|
||||
try {
|
||||
updateClosestNearbyCardViewInfo();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Hide nearby notification card view if related shared preferences is false
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Notification Count and Campaigns should not be set, if it is used in User Profile
|
||||
|
|
@ -435,83 +499,97 @@ public class ContributionsFragment
|
|||
fetchCampaigns();
|
||||
}
|
||||
}
|
||||
mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI);
|
||||
}
|
||||
|
||||
private void checkPermissionsAndShowNearbyCardView() {
|
||||
if (PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
|
||||
onLocationPermissionGranted();
|
||||
} else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
||||
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
|
||||
nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
||||
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
|
||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
||||
showNearbyCardPermissionRationale();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestLocationPermission() {
|
||||
PermissionUtils.checkPermissionsAndPerformAction(getActivity(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
this::onLocationPermissionGranted,
|
||||
this::displayYouWontSeeNearbyMessage,
|
||||
-1,
|
||||
-1);
|
||||
nearbyLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION});
|
||||
}
|
||||
|
||||
private void onLocationPermissionGranted() {
|
||||
nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED;
|
||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED;
|
||||
locationManager.registerLocationManager();
|
||||
}
|
||||
|
||||
private void showNearbyCardPermissionRationale() {
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.nearby_card_permission_title),
|
||||
getString(R.string.nearby_card_permission_explanation),
|
||||
this::requestLocationPermission,
|
||||
this::displayYouWontSeeNearbyMessage,
|
||||
checkBoxView,
|
||||
false);
|
||||
getString(R.string.nearby_card_permission_title),
|
||||
getString(R.string.nearby_card_permission_explanation),
|
||||
this::requestLocationPermission,
|
||||
this::displayYouWontSeeNearbyMessage,
|
||||
checkBoxView,
|
||||
false);
|
||||
}
|
||||
|
||||
private void displayYouWontSeeNearbyMessage() {
|
||||
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.unable_to_display_nearest_place));
|
||||
ViewUtil.showLongToast(getActivity(),
|
||||
getResources().getString(R.string.unable_to_display_nearest_place));
|
||||
// Set to true as the user doesn't want the app to ask for location permission anymore
|
||||
store.putBoolean("doNotAskForLocationPermission", true);
|
||||
}
|
||||
|
||||
|
||||
private void updateClosestNearbyCardViewInfo() {
|
||||
curLatLng = locationManager.getLastLocation();
|
||||
currentLatLng = locationManager.getLastLocation();
|
||||
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
|
||||
.loadAttractionsFromLocation(curLatLng, curLatLng, true, false, false)) // thanks to boolean, it will only return closest result
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNearbyNotification,
|
||||
throwable -> {
|
||||
Timber.d(throwable);
|
||||
updateNearbyNotification(null);
|
||||
}));
|
||||
.loadAttractionsFromLocation(currentLatLng, currentLatLng, true,
|
||||
false)) // thanks to boolean, it will only return closest result
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNearbyNotification,
|
||||
throwable -> {
|
||||
Timber.d(throwable);
|
||||
updateNearbyNotification(null);
|
||||
}));
|
||||
}
|
||||
|
||||
private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) {
|
||||
Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0);
|
||||
String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location);
|
||||
closestNearbyPlace.setDistance(distance);
|
||||
nearbyNotificationCardView.updateContent(closestNearbyPlace);
|
||||
private void updateNearbyNotification(
|
||||
@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null
|
||||
&& nearbyPlacesInfo.placeList.size() > 0) {
|
||||
Place closestNearbyPlace = null;
|
||||
// Find the first nearby place that has no image and exists
|
||||
for (Place place : nearbyPlacesInfo.placeList) {
|
||||
if (place.pic.equals("") && place.exists) {
|
||||
closestNearbyPlace = place;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestNearbyPlace == null) {
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
} else {
|
||||
String distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location);
|
||||
closestNearbyPlace.setDistance(distance);
|
||||
direction = (float) computeBearing(currentLatLng, closestNearbyPlace.location);
|
||||
binding.cardViewNearby.updateContent(closestNearbyPlace);
|
||||
}
|
||||
} else {
|
||||
// Means that no close nearby place is found
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731
|
||||
if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) {
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try{
|
||||
try {
|
||||
compositeDisposable.clear();
|
||||
getChildFragmentManager().removeOnBackStackChangedListener(this);
|
||||
locationManager.unregisterLocationManager();
|
||||
|
|
@ -525,22 +603,17 @@ public class ContributionsFragment
|
|||
@Override
|
||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
||||
// Will be called if location changed more than 1000 meter
|
||||
// Do nothing on slight changes for using network efficiently
|
||||
firstLocationUpdate = false;
|
||||
updateClosestNearbyCardViewInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChangedSlightly(LatLng latLng) {
|
||||
/* Update closest nearby notification card onLocationChangedSlightly
|
||||
If first time to update location after onResume, then no need to wait for significant
|
||||
location change. Any closest location is better than no location
|
||||
*/
|
||||
if (firstLocationUpdate) {
|
||||
*/
|
||||
try {
|
||||
updateClosestNearbyCardViewInfo();
|
||||
// Turn it to false, since it is not first location update anymore. To change closest location
|
||||
// notification, we need to wait for a significant location change.
|
||||
firstLocationUpdate = false;
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +623,8 @@ public class ContributionsFragment
|
|||
updateClosestNearbyCardViewInfo();
|
||||
}
|
||||
|
||||
@Override public void onViewCreated(@NonNull View view,
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
|
@ -562,26 +636,35 @@ public class ContributionsFragment
|
|||
*/
|
||||
private void fetchCampaigns() {
|
||||
if (Utils.isMonumentsEnabled(new Date())) {
|
||||
campaignView.setCampaign(wlmCampaign);
|
||||
campaignView.setVisibility(View.VISIBLE);
|
||||
if (binding!=null) {
|
||||
binding.campaignsView.setCampaign(wlmCampaign);
|
||||
binding.campaignsView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) {
|
||||
presenter.getCampaigns();
|
||||
} else {
|
||||
campaignView.setVisibility(View.GONE);
|
||||
if (binding!=null) {
|
||||
binding.campaignsView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void showMessage(String message) {
|
||||
@Override
|
||||
public void showMessage(String message) {
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override public void showCampaigns(Campaign campaign) {
|
||||
@Override
|
||||
public void showCampaigns(Campaign campaign) {
|
||||
if (campaign != null && !isUserProfile) {
|
||||
campaignView.setCampaign(campaign);
|
||||
if (binding!=null) {
|
||||
binding.campaignsView.setCampaign(campaign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
presenter.onDetachView();
|
||||
}
|
||||
|
|
@ -593,6 +676,17 @@ public class ContributionsFragment
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the upload process for a contribution
|
||||
*
|
||||
* @param contribution
|
||||
*/
|
||||
public void restartUpload(Contribution contribution) {
|
||||
contribution.setState(Contribution.STATE_QUEUED);
|
||||
contributionsPresenter.saveContribution(contribution);
|
||||
Timber.d("Restarting for %s", contribution.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry upload when it is failed
|
||||
*
|
||||
|
|
@ -601,10 +695,25 @@ public class ContributionsFragment
|
|||
@Override
|
||||
public void retryUpload(Contribution contribution) {
|
||||
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
|
||||
contribution.setState(Contribution.STATE_QUEUED);
|
||||
contributionsPresenter.saveContribution(contribution);
|
||||
Timber.d("Restarting for %s", contribution.toString());
|
||||
if (contribution.getState() == STATE_PAUSED
|
||||
|| contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
|
||||
restartUpload(contribution);
|
||||
} else if (contribution.getState() == STATE_FAILED) {
|
||||
int retries = contribution.getRetries();
|
||||
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
|
||||
/* Limit the number of retries for a failed upload
|
||||
to handle cases like invalid filename as such uploads
|
||||
will never be successful */
|
||||
if (retries < MAX_RETRIES) {
|
||||
contribution.setRetries(retries + 1);
|
||||
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
|
||||
retries + 1);
|
||||
restartUpload(contribution);
|
||||
} else {
|
||||
// TODO: Show the exact reason for failure
|
||||
Toast.makeText(getContext(),
|
||||
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
|
||||
}
|
||||
|
|
@ -616,6 +725,7 @@ public class ContributionsFragment
|
|||
|
||||
/**
|
||||
* Pauses the upload
|
||||
*
|
||||
* @param contribution
|
||||
*/
|
||||
@Override
|
||||
|
|
@ -639,15 +749,15 @@ public class ContributionsFragment
|
|||
|
||||
/**
|
||||
* Replace whatever is in the current contributionsFragmentContainer view with
|
||||
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a
|
||||
* contribution.
|
||||
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects
|
||||
* a contribution.
|
||||
*/
|
||||
@Override
|
||||
public void showDetail(int position, boolean isWikipediaButtonDisplayed) {
|
||||
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
||||
mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
|
||||
if(isUserProfile) {
|
||||
((ProfileActivity)getActivity()).setScroll(false);
|
||||
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
||||
if (isUserProfile) {
|
||||
((ProfileActivity) getActivity()).setScroll(false);
|
||||
}
|
||||
showMediaDetailPagerFragment();
|
||||
}
|
||||
|
|
@ -672,24 +782,26 @@ public class ContributionsFragment
|
|||
public boolean backButtonClicked() {
|
||||
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
|
||||
if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) {
|
||||
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
nearbyNotificationCardView.setVisibility(View.VISIBLE);
|
||||
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
binding.cardViewNearby.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
binding.cardViewNearby.setVisibility(View.GONE);
|
||||
}
|
||||
removeFragment(mediaDetailPagerFragment);
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, mediaDetailPagerFragment);
|
||||
if(isUserProfile) {
|
||||
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG,
|
||||
mediaDetailPagerFragment);
|
||||
if (isUserProfile) {
|
||||
// Fragment is associated with ProfileActivity
|
||||
// Enable ParentViewPager Scroll
|
||||
((ProfileActivity)getActivity()).setScroll(true);
|
||||
}else {
|
||||
((ProfileActivity) getActivity()).setScroll(true);
|
||||
} else {
|
||||
fetchCampaigns();
|
||||
}
|
||||
if (getActivity() instanceof MainActivity) {
|
||||
// Fragment is associated with MainActivity
|
||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
((BaseActivity) getActivity()).getSupportActionBar()
|
||||
.setDisplayHomeAsUpEnabled(false);
|
||||
((MainActivity) getActivity()).showTabs();
|
||||
}
|
||||
return true;
|
||||
|
|
@ -709,11 +821,11 @@ public class ContributionsFragment
|
|||
void upDateUploadCount() {
|
||||
WorkManager.getInstance(getContext())
|
||||
.getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe(
|
||||
getViewLifecycleOwner(), workInfos -> {
|
||||
if (workInfos.size() > 0) {
|
||||
setUploadCount();
|
||||
}
|
||||
});
|
||||
getViewLifecycleOwner(), workInfos -> {
|
||||
if (workInfos.size() > 0) {
|
||||
setUploadCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -724,29 +836,40 @@ public class ContributionsFragment
|
|||
*/
|
||||
@Override
|
||||
public void refreshNominatedMedia(int index) {
|
||||
if(mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) {
|
||||
if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) {
|
||||
removeFragment(mediaDetailPagerFragment);
|
||||
mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
|
||||
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
||||
mediaDetailPagerFragment.showImage(index);
|
||||
showMediaDetailPagerFragment();
|
||||
}
|
||||
}
|
||||
|
||||
// click listener to toggle description that means uses can press the limited connection
|
||||
// banner and description will hide. Tap again to show description.
|
||||
private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() {
|
||||
// click listener to toggle description that means uses can press the limited connection
|
||||
// banner and description will hide. Tap again to show description.
|
||||
private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
View view2 = limitedConnectionDescriptionTextView;
|
||||
if (view2.getVisibility() == View.GONE) {
|
||||
view2.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
view2.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
View view2 = binding.limitedConnectionDescriptionTextView;
|
||||
if (view2.getVisibility() == View.GONE) {
|
||||
view2.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
view2.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
|
||||
*/
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
float rotateDegree = Math.round(event.values[0]);
|
||||
binding.cardViewNearby.rotateCompass(rotateDegree, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The contract for Contributions list View & Presenter
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import static android.view.View.GONE;
|
|||
import static android.view.View.VISIBLE;
|
||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
|
|
@ -16,11 +17,12 @@ import android.view.ViewGroup;
|
|||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
@ -28,26 +30,25 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
|
|||
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -60,63 +61,72 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
|
||||
private static final String RV_STATE = "rv_scroll_state";
|
||||
|
||||
@BindView(R.id.contributionsList)
|
||||
RecyclerView rvContributionsList;
|
||||
@BindView(R.id.loadingContributionsProgressBar)
|
||||
ProgressBar progressBar;
|
||||
@BindView(R.id.fab_plus)
|
||||
FloatingActionButton fabPlus;
|
||||
@BindView(R.id.fab_camera)
|
||||
FloatingActionButton fabCamera;
|
||||
@BindView(R.id.fab_gallery)
|
||||
FloatingActionButton fabGallery;
|
||||
@BindView(R.id.noContributionsYet)
|
||||
TextView noContributionsYet;
|
||||
@BindView(R.id.fab_layout)
|
||||
LinearLayout fab_layout;
|
||||
@BindView(R.id.fab_custom_gallery)
|
||||
FloatingActionButton fabCustomGallery;
|
||||
|
||||
@Inject
|
||||
SystemThemeUtils systemThemeUtils;
|
||||
@BindView(R.id.tv_contributions_of_user)
|
||||
AppCompatTextView tvContributionsOfUser;
|
||||
|
||||
@Inject
|
||||
ContributionController controller;
|
||||
@Inject
|
||||
MediaClient mediaClient;
|
||||
|
||||
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
||||
@Inject
|
||||
WikiSite languageWikipediaSite;
|
||||
|
||||
@Inject
|
||||
ContributionsListPresenter contributionsListPresenter;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private FragmentContributionsListBinding binding;
|
||||
private Animation fab_close;
|
||||
private Animation fab_open;
|
||||
private Animation rotate_forward;
|
||||
private Animation rotate_backward;
|
||||
|
||||
|
||||
private boolean isFabOpen;
|
||||
|
||||
private ContributionsListAdapter adapter;
|
||||
@VisibleForTesting
|
||||
protected RecyclerView rvContributionsList;
|
||||
|
||||
@Nullable private Callback callback;
|
||||
@VisibleForTesting
|
||||
protected ContributionsListAdapter adapter;
|
||||
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
protected Callback callback;
|
||||
|
||||
private final int SPAN_COUNT_LANDSCAPE = 3;
|
||||
private final int SPAN_COUNT_PORTRAIT = 1;
|
||||
|
||||
private int contributionsSize;
|
||||
String userName;
|
||||
private String userName;
|
||||
|
||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||
new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
@Override
|
||||
public void onActivityResult(Map<String, Boolean> result) {
|
||||
boolean areAllGranted = true;
|
||||
for (final boolean b : result.values()) {
|
||||
areAllGranted = areAllGranted && b;
|
||||
}
|
||||
|
||||
if (areAllGranted) {
|
||||
controller.locationPermissionCallback.onLocationPermissionGranted();
|
||||
} else {
|
||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||
controller.handleShowRationaleFlowCameraLocation(getActivity(),
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
} else {
|
||||
controller.locationPermissionCallback.onLocationPermissionDenied(
|
||||
getActivity().getString(
|
||||
R.string.in_app_camera_location_permission_denied));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) {
|
||||
public void onCreate(
|
||||
@Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
//Now that we are allowing this fragment to be started for
|
||||
// any userName- we expect it to be passed as an argument
|
||||
|
|
@ -133,21 +143,36 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
public View onCreateView(
|
||||
final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
binding = FragmentContributionsListBinding.inflate(
|
||||
inflater, container, false
|
||||
);
|
||||
rvContributionsList = binding.contributionsList;
|
||||
|
||||
contributionsListPresenter.onAttachView(this);
|
||||
binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector());
|
||||
binding.fabCustomGallery.setOnLongClickListener(view -> {
|
||||
ViewUtil.showShortToast(getContext(),R.string.custom_selector_title);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (Objects.equals(sessionManager.getUserName(), userName)) {
|
||||
tvContributionsOfUser.setVisibility(GONE);
|
||||
fab_layout.setVisibility(VISIBLE);
|
||||
binding.tvContributionsOfUser.setVisibility(GONE);
|
||||
binding.fabLayout.setVisibility(VISIBLE);
|
||||
} else {
|
||||
tvContributionsOfUser.setVisibility(VISIBLE);
|
||||
tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
|
||||
fab_layout.setVisibility(GONE);
|
||||
binding.tvContributionsOfUser.setVisibility(VISIBLE);
|
||||
binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
|
||||
binding.fabLayout.setVisibility(GONE);
|
||||
}
|
||||
|
||||
initAdapter();
|
||||
return view;
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -280,7 +305,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
public void onConfigurationChanged(final Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
// check orientation
|
||||
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
||||
binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
||||
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
||||
rvContributionsList
|
||||
.setLayoutManager(
|
||||
|
|
@ -295,22 +320,29 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
}
|
||||
|
||||
private void setListeners() {
|
||||
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||
fabCamera.setOnClickListener(view -> {
|
||||
controller.initiateCameraPick(getActivity());
|
||||
binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||
binding.fabCamera.setOnClickListener(view -> {
|
||||
controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||
animateFAB(isFabOpen);
|
||||
});
|
||||
fabGallery.setOnClickListener(view -> {
|
||||
binding.fabCamera.setOnLongClickListener(view -> {
|
||||
ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera);
|
||||
return true;
|
||||
});
|
||||
binding.fabGallery.setOnClickListener(view -> {
|
||||
controller.initiateGalleryPick(getActivity(), true);
|
||||
animateFAB(isFabOpen);
|
||||
});
|
||||
binding.fabGallery.setOnLongClickListener(view -> {
|
||||
ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch Custom Selector.
|
||||
*/
|
||||
@OnClick(R.id.fab_custom_gallery)
|
||||
void launchCustomSelector(){
|
||||
protected void launchCustomSelector() {
|
||||
controller.initiateCustomGalleryPickWithPermission(getActivity());
|
||||
animateFAB(isFabOpen);
|
||||
}
|
||||
|
|
@ -321,25 +353,25 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
|
||||
private void animateFAB(final boolean isFabOpen) {
|
||||
this.isFabOpen = !isFabOpen;
|
||||
if (fabPlus.isShown()) {
|
||||
if (isFabOpen) {
|
||||
fabPlus.startAnimation(rotate_backward);
|
||||
fabCamera.startAnimation(fab_close);
|
||||
fabGallery.startAnimation(fab_close);
|
||||
fabCustomGallery.startAnimation(fab_close);
|
||||
fabCamera.hide();
|
||||
fabGallery.hide();
|
||||
fabCustomGallery.hide();
|
||||
} else {
|
||||
fabPlus.startAnimation(rotate_forward);
|
||||
fabCamera.startAnimation(fab_open);
|
||||
fabGallery.startAnimation(fab_open);
|
||||
fabCustomGallery.startAnimation(fab_open);
|
||||
fabCamera.show();
|
||||
fabGallery.show();
|
||||
fabCustomGallery.show();
|
||||
}
|
||||
this.isFabOpen = !isFabOpen;
|
||||
if (binding.fabPlus.isShown()) {
|
||||
if (isFabOpen) {
|
||||
binding.fabPlus.startAnimation(rotate_backward);
|
||||
binding.fabCamera.startAnimation(fab_close);
|
||||
binding.fabGallery.startAnimation(fab_close);
|
||||
binding.fabCustomGallery.startAnimation(fab_close);
|
||||
binding.fabCamera.hide();
|
||||
binding.fabGallery.hide();
|
||||
binding.fabCustomGallery.hide();
|
||||
} else {
|
||||
binding.fabPlus.startAnimation(rotate_forward);
|
||||
binding.fabCamera.startAnimation(fab_open);
|
||||
binding.fabGallery.startAnimation(fab_open);
|
||||
binding.fabCustomGallery.startAnimation(fab_open);
|
||||
binding.fabCamera.show();
|
||||
binding.fabGallery.show();
|
||||
binding.fabCustomGallery.show();
|
||||
}
|
||||
this.isFabOpen = !isFabOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +380,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
*/
|
||||
@Override
|
||||
public void showWelcomeTip(final boolean shouldShow) {
|
||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -358,12 +390,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
*/
|
||||
@Override
|
||||
public void showProgress(final boolean shouldShow) {
|
||||
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showNoContributionsUI(final boolean shouldShow) {
|
||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -393,14 +425,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
@Override
|
||||
public void deleteUpload(final Contribution contribution) {
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
String.format(getString(R.string.cancelling_upload),
|
||||
Locale.getDefault().getDisplayLanguage()),
|
||||
String.format(getString(R.string.cancel_upload_dialog),
|
||||
Locale.getDefault().getDisplayLanguage()),
|
||||
"YES", "NO",
|
||||
String.format(Locale.getDefault(),
|
||||
getString(R.string.cancelling_upload)),
|
||||
String.format(Locale.getDefault(),
|
||||
getString(R.string.cancel_upload_dialog)),
|
||||
String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)),
|
||||
() -> {
|
||||
ViewUtil.showShortToast(getContext(), R.string.cancelling_upload);
|
||||
contributionsListPresenter.deleteUpload(contribution);
|
||||
CommonsApplication.cancelledUploads.add(contribution.getPageId());
|
||||
}, () -> {
|
||||
// Do nothing
|
||||
});
|
||||
|
|
@ -422,8 +455,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
public void addImageToWikipedia(Contribution contribution) {
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.add_picture_to_wikipedia_article_title),
|
||||
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
|
||||
Locale.getDefault().getDisplayLanguage()),
|
||||
getString(R.string.add_picture_to_wikipedia_article_desc),
|
||||
() -> {
|
||||
showAddImageToWikipediaInstructions(contribution);
|
||||
}, () -> {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import fr.free.nrw.commons.MediaDataExtractor;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -76,11 +70,7 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
compositeDisposable.add(repository
|
||||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe(() -> {
|
||||
WorkManager.getInstance(view.getContext().getApplicationContext())
|
||||
.enqueueUniqueWork(
|
||||
UploadWorker.class.getSimpleName(),
|
||||
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
|
||||
}));
|
||||
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
||||
view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.databinding.MainBinding;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
|
|
@ -45,9 +41,13 @@ import fr.free.nrw.commons.notification.NotificationController;
|
|||
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
|
@ -59,14 +59,8 @@ public class MainActivity extends BaseActivity
|
|||
SessionManager sessionManager;
|
||||
@Inject
|
||||
ContributionController controller;
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
@BindView(R.id.pager)
|
||||
public UnswipableViewPager viewPager;
|
||||
@BindView(R.id.fragmentContainer)
|
||||
public FrameLayout fragmentContainer;
|
||||
@BindView(R.id.fragment_main_nav_tab_layout)
|
||||
NavTabLayout tabLayout;
|
||||
@Inject
|
||||
ContributionDao contributionDao;
|
||||
|
||||
private ContributionsFragment contributionsFragment;
|
||||
private NearbyParentFragment nearbyParentFragment;
|
||||
|
|
@ -91,6 +85,11 @@ public class MainActivity extends BaseActivity
|
|||
|
||||
public Menu menu;
|
||||
|
||||
public MainBinding binding;
|
||||
|
||||
NavTabLayout tabLayout;
|
||||
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
*
|
||||
|
|
@ -118,11 +117,13 @@ public class MainActivity extends BaseActivity
|
|||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = MainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
tabLayout = binding.fragmentMainNavTabLayout;
|
||||
loadLocale();
|
||||
setContentView(R.layout.main);
|
||||
ButterKnife.bind(this);
|
||||
setSupportActionBar(toolbar);
|
||||
toolbar.setNavigationOnClickListener(view -> {
|
||||
|
||||
binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> {
|
||||
onSupportNavigateUp();
|
||||
});
|
||||
/*
|
||||
|
|
@ -139,6 +140,10 @@ public class MainActivity extends BaseActivity
|
|||
setTitle(getString(R.string.navigation_item_explore));
|
||||
setUpLoggedOutPager();
|
||||
} else {
|
||||
if (applicationKvStore.getBoolean("firstrun", true)) {
|
||||
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
|
||||
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
|
||||
}
|
||||
if(savedInstanceState == null){
|
||||
//starting a fresh fragment.
|
||||
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
|
||||
|
|
@ -152,15 +157,29 @@ public class MainActivity extends BaseActivity
|
|||
}
|
||||
}
|
||||
setUpPager();
|
||||
/**
|
||||
* Ask the user for media location access just after login
|
||||
* so that location in the EXIF metadata of the images shared by the user
|
||||
* is retained on devices running Android 10 or above
|
||||
*/
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||
PermissionUtils.checkPermissionsAndPerformAction(
|
||||
this,
|
||||
() -> {},
|
||||
R.string.media_location_permission_denied,
|
||||
R.string.add_location_manually,
|
||||
permission.ACCESS_MEDIA_LOCATION);
|
||||
}
|
||||
checkAndResumeStuckUploads();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedItemId(int id) {
|
||||
tabLayout.setSelectedItemId(id);
|
||||
binding.fragmentMainNavTabLayout.setSelectedItemId(id);
|
||||
}
|
||||
|
||||
private void setUpPager() {
|
||||
tabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
|
||||
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
|
||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
||||
// do not change title for more fragment
|
||||
setTitle(item.getTitle());
|
||||
|
|
@ -175,7 +194,7 @@ public class MainActivity extends BaseActivity
|
|||
|
||||
private void setUpLoggedOutPager() {
|
||||
loadFragment(ExploreFragment.newInstance(),false);
|
||||
tabLayout.setOnNavigationItemSelectedListener(item -> {
|
||||
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> {
|
||||
if (!item.getTitle().equals(getString(R.string.more))) {
|
||||
// do not change title for more fragment
|
||||
setTitle(item.getTitle());
|
||||
|
|
@ -237,11 +256,11 @@ public class MainActivity extends BaseActivity
|
|||
}
|
||||
|
||||
public void hideTabs() {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
binding.fragmentMainNavTabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void showTabs() {
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.fragmentMainNavTabLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -259,6 +278,34 @@ public class MainActivity extends BaseActivity
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the uploads that got stuck because of the app being killed
|
||||
* or the device being rebooted.
|
||||
*
|
||||
* When the app is terminated or the device is restarted, contributions remain in the
|
||||
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events.
|
||||
* So, retrieving contributions labeled as 'STATE_IN_PROGRESS'
|
||||
* from the database will provide the list of uploads that appear as stuck on opening the app again
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private void checkAndResumeStuckUploads() {
|
||||
List<Contribution> stuckUploads = contributionDao.getContribution(
|
||||
Collections.singletonList(Contribution.STATE_IN_PROGRESS))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
Timber.d("Resuming " + stuckUploads.size() + " uploads...");
|
||||
if(!stuckUploads.isEmpty()) {
|
||||
for(Contribution contribution: stuckUploads) {
|
||||
contribution.setState(Contribution.STATE_QUEUED);
|
||||
Completable.fromAction(() -> contributionDao.saveSynchronous(contribution))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
}
|
||||
WorkRequestHelper.Companion.makeOneTimeWorkRequest(
|
||||
this, ExistingWorkPolicy.APPEND_OR_REPLACE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
|
@ -268,7 +315,7 @@ public class MainActivity extends BaseActivity
|
|||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
|
||||
outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem());
|
||||
outState.putString("activeFragment", activeFragment.name());
|
||||
}
|
||||
|
||||
|
|
@ -347,6 +394,21 @@ public class MainActivity extends BaseActivity
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry all failed uploads as soon as the user returns to the app
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private void retryAllFailedUploads() {
|
||||
contributionDao.
|
||||
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(failedUploads -> {
|
||||
for (Contribution contribution: failedUploads) {
|
||||
contributionsFragment.retryUpload(contribution);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void toggleLimitedConnectionMode() {
|
||||
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
||||
!defaultKvStore
|
||||
|
|
@ -356,10 +418,8 @@ public class MainActivity extends BaseActivity
|
|||
viewUtilWrapper
|
||||
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
|
||||
} else {
|
||||
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
|
||||
UploadWorker.class.getSimpleName(),
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
|
||||
|
||||
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE);
|
||||
viewUtilWrapper
|
||||
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
|
||||
}
|
||||
|
|
@ -368,8 +428,6 @@ public class MainActivity extends BaseActivity
|
|||
public void centerMapToPlace(Place place) {
|
||||
setSelectedItemId(NavTab.NEARBY.code());
|
||||
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() {
|
||||
// if mapBox initialize in nearbyParentFragment then MapReady() function called
|
||||
// so that nearbyParentFragemt.centerMaptoPlace(place) not throw any null pointer exception
|
||||
@Override
|
||||
public void onReady() {
|
||||
nearbyParentFragment.centerMapToPlace(place);
|
||||
|
|
@ -390,8 +448,11 @@ public class MainActivity extends BaseActivity
|
|||
|
||||
if ((applicationKvStore.getBoolean("firstrun", true)) &&
|
||||
(!applicationKvStore.getBoolean("login_skipped"))) {
|
||||
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
|
||||
WelcomeActivity.startYourself(this);
|
||||
}
|
||||
|
||||
retryAllFailedUploads();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -407,7 +468,7 @@ public class MainActivity extends BaseActivity
|
|||
* Public method to show nearby from the reference of this.
|
||||
*/
|
||||
public void showNearby() {
|
||||
tabLayout.setSelectedItemId(NavTab.NEARBY.code());
|
||||
binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code());
|
||||
}
|
||||
|
||||
public enum ActiveFragment {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.WallpaperManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.datasource.DataSource;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
import fr.free.nrw.commons.R;
|
||||
import java.io.IOException;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SetWallpaperWorker extends Worker {
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
public SetWallpaperWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||
super(context, params);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Context context = getApplicationContext();
|
||||
createNotificationChannel(context);
|
||||
showProgressNotification(context);
|
||||
|
||||
String imageUrl = getInputData().getString("imageUrl");
|
||||
if (imageUrl == null) {
|
||||
return Result.failure();
|
||||
}
|
||||
|
||||
ImageRequest imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(Uri.parse(imageUrl))
|
||||
.build();
|
||||
|
||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||
final DataSource<CloseableReference<CloseableImage>>
|
||||
dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
|
||||
|
||||
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
if (dataSource.isFinished() && bitmap != null) {
|
||||
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
||||
setWallpaper(context, Bitmap.createBitmap(bitmap));
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
|
||||
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.");
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}, CallerThreadExecutor.getInstance());
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private void setWallpaper(Context context, Bitmap bitmap) {
|
||||
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
|
||||
|
||||
try {
|
||||
wallpaperManager.setBitmap(bitmap);
|
||||
showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully.");
|
||||
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Error setting wallpaper");
|
||||
showNotification(context, "Setting Wallpaper Failed", " "+e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showProgressNotification(Context context) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.commons_logo)
|
||||
.setContentTitle("Setting Wallpaper")
|
||||
.setContentText("Please wait...")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setOngoing(true)
|
||||
.setProgress(0, 0, true);
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void showNotification(Context context, String title, String content) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.commons_logo)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setOngoing(false);
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void createNotificationChannel(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Wallpaper Setting";
|
||||
String description = "Notifications for wallpaper setting progress";
|
||||
int importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
|
||||
private val SWIPE_VELOCITY_THRESHOLD = 1000
|
||||
|
||||
override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
|
||||
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean {
|
||||
return gestureDetector.onTouchEvent(motionEvent)
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
|
||||
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
override fun onDown(e: MotionEvent?): Boolean {
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.bumptech.glide.Glide
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
|
||||
|
|
@ -85,6 +86,8 @@ class ImageAdapter(
|
|||
*/
|
||||
private var actionableImagesMap: TreeMap<Int, Image> = TreeMap()
|
||||
|
||||
private var uploadingContributionList: List<Contribution> = ArrayList()
|
||||
|
||||
/**
|
||||
* Stores already added positions of actionable images
|
||||
*/
|
||||
|
|
@ -119,6 +122,7 @@ class ImageAdapter(
|
|||
* Bind View holder, load image, selected view, click listeners.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
|
||||
var image=images[position]
|
||||
holder.image.setImageDrawable (null)
|
||||
if (context.contentResolver.getType(image.uri) == null) {
|
||||
|
|
@ -151,13 +155,12 @@ class ImageAdapter(
|
|||
|
||||
val isSelected = selectedIndex != -1
|
||||
if (isSelected) {
|
||||
holder.itemSelected(selectedImages.size)
|
||||
holder.itemSelected()
|
||||
} else {
|
||||
holder.itemUnselected()
|
||||
}
|
||||
|
||||
imageLoader.queryAndSetView(
|
||||
holder, image, ioDispatcher, defaultDispatcher
|
||||
holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList
|
||||
)
|
||||
scope.launch {
|
||||
val sharedPreferences: SharedPreferences =
|
||||
|
|
@ -168,15 +171,17 @@ class ImageAdapter(
|
|||
// If the position is not already visited, that means the position is new then
|
||||
// finds the next actionable image position from all images
|
||||
if (!alreadyAddedPositions.contains(position)) {
|
||||
processThumbnailForActionedImage(holder, position)
|
||||
processThumbnailForActionedImage(holder, position, uploadingContributionList)
|
||||
|
||||
// If the position is already visited, that means the image is already present
|
||||
// inside map, so it will fetch the image from the map and load in the holder
|
||||
} else {
|
||||
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
|
||||
image = actionableImages[position]
|
||||
Glide.with(holder.image).load(image.uri)
|
||||
.thumbnail(0.3f).into(holder.image)
|
||||
if(actionableImages.size > position) {
|
||||
image = actionableImages[position]
|
||||
Glide.with(holder.image).load(image.uri)
|
||||
.thumbnail(0.3f).into(holder.image)
|
||||
}
|
||||
}
|
||||
|
||||
// If switch is turned off, it just fetches the image from all images without any
|
||||
|
|
@ -204,11 +209,12 @@ class ImageAdapter(
|
|||
*/
|
||||
suspend fun processThumbnailForActionedImage(
|
||||
holder: ImageViewHolder,
|
||||
position: Int
|
||||
position: Int,
|
||||
uploadingContributionList: List<Contribution>
|
||||
) {
|
||||
val next = imageLoader.nextActionableImage(
|
||||
allImages, ioDispatcher, defaultDispatcher,
|
||||
nextImagePosition
|
||||
nextImagePosition, uploadingContributionList
|
||||
)
|
||||
|
||||
// If next actionable image is found, saves it, as the the search for
|
||||
|
|
@ -328,12 +334,13 @@ class ImageAdapter(
|
|||
/**
|
||||
* Initialize the data set.
|
||||
*/
|
||||
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) {
|
||||
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>, uploadedImages: List<Contribution> = ArrayList()) {
|
||||
allImages = fixedImages
|
||||
val oldImageList:ArrayList<Image> = images
|
||||
val newImageList:ArrayList<Image> = ArrayList(newImages)
|
||||
actionableImagesMap = emptyMap
|
||||
alreadyAddedPositions = ArrayList()
|
||||
uploadingContributionList = uploadedImages
|
||||
nextImagePosition = 0
|
||||
reachedEndOfFolder = false
|
||||
selectedImages = ArrayList()
|
||||
|
|
@ -355,15 +362,56 @@ class ImageAdapter(
|
|||
/**
|
||||
* Refresh the data in the adapter
|
||||
*/
|
||||
fun refresh(newImages: List<Image>, fixedImages: List<Image>) {
|
||||
fun refresh(newImages: List<Image>, fixedImages: List<Image>, uploadingImages: List<Contribution> = ArrayList()) {
|
||||
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||
selectedImages.clear()
|
||||
images.clear()
|
||||
selectedImages = arrayListOf()
|
||||
init(newImages, fixedImages, TreeMap())
|
||||
init(newImages, fixedImages, TreeMap(),uploadingImages)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear selected images and empty the list.
|
||||
*/
|
||||
fun clearSelectedImages(){
|
||||
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||
selectedImages.clear()
|
||||
selectedImages = arrayListOf()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove image from actionable images map.
|
||||
*/
|
||||
fun removeImageFromActionableImageMap(image: Image) {
|
||||
val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||
val showAlreadyActionedImages =
|
||||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
|
||||
if(showAlreadyActionedImages) {
|
||||
refresh(allImages, allImages, uploadingContributionList)
|
||||
} else {
|
||||
val iterator = actionableImagesMap.entries.iterator()
|
||||
var index = 0
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
if (entry.value == image) {
|
||||
imagePositionAsPerIncreasingOrder -= 1
|
||||
iterator.remove()
|
||||
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
|
||||
notifyItemRemoved(index)
|
||||
notifyItemRangeChanged(index, itemCount )
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the data set held by the adapter.
|
||||
*
|
||||
|
|
@ -407,17 +455,16 @@ class ImageAdapter(
|
|||
*/
|
||||
class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
|
||||
private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
|
||||
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
|
||||
private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group)
|
||||
private val notForUploadGroup: Group = itemView.findViewById(R.id.not_for_upload_group)
|
||||
private val selectedGroup: Group = itemView.findViewById(R.id.selected_group)
|
||||
|
||||
/**
|
||||
* Item selected view.
|
||||
*/
|
||||
fun itemSelected(index: Int) {
|
||||
fun itemSelected() {
|
||||
selectedGroup.visibility = View.VISIBLE
|
||||
selectedNumber.text = index.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -434,6 +481,13 @@ class ImageAdapter(
|
|||
uploadedGroup.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Item is uploading
|
||||
*/
|
||||
fun itemUploading() {
|
||||
uploadingGroup.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Item is not for upload view
|
||||
*/
|
||||
|
|
@ -452,6 +506,13 @@ class ImageAdapter(
|
|||
return notForUploadGroup.visibility == View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Item is not uploading
|
||||
*/
|
||||
fun itemNotUploading() {
|
||||
uploadingGroup.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Item Not Uploaded view.
|
||||
*/
|
||||
|
|
@ -513,4 +574,4 @@ class ImageAdapter(
|
|||
return images[position].date
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper
|
|||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.lang.Integer.max
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
|
@ -66,6 +67,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
/**
|
||||
* Maximum number of images that can be selected.
|
||||
*/
|
||||
private val uploadLimit: Int = 20
|
||||
|
||||
/**
|
||||
* Flag that is marked true when the amount
|
||||
* of selected images is greater than the upload limit.
|
||||
*/
|
||||
private var uploadLimitExceeded: Boolean = false
|
||||
|
||||
/**
|
||||
* Tracks the amount by which the upload limit has been exceeded.
|
||||
*/
|
||||
private var uploadLimitExceededBy: Int = 0
|
||||
|
||||
/**
|
||||
* View Model Factory.
|
||||
*/
|
||||
|
|
@ -95,6 +112,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
var imageFragment: ImageFragment? = null
|
||||
|
||||
private var progressDialogText:String=""
|
||||
|
||||
/**
|
||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||
*/
|
||||
|
|
@ -140,7 +159,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
data!!
|
||||
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
|
||||
val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false)
|
||||
imageFragment!!.passSelectedImages(selectedImages, shouldRefresh)
|
||||
imageFragment?.passSelectedImages(selectedImages, shouldRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,17 +206,18 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
markAsNotForUpload(arrayListOf())
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
while (i < selectedImages.size) {
|
||||
val path = selectedImages[i].path
|
||||
|
||||
val iterator = selectedImages.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val image = iterator.next()
|
||||
val path = image.path
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
selectedImages.removeAt(i)
|
||||
i--
|
||||
iterator.remove()
|
||||
}
|
||||
i++
|
||||
}
|
||||
markAsNotForUpload(selectedImages)
|
||||
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,56 +241,63 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
private fun insertIntoNotForUpload(images: ArrayList<Image>) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
imageFragment?.showMarkUnmarkProgressDialog(text = progressDialogText)
|
||||
}
|
||||
|
||||
var allImagesAlreadyNotForUpload = true
|
||||
images.forEach {
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
it.uri,
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
val exists = notForUploadStatusDao.find(imageSHA1)
|
||||
|
||||
// If image exists in not for upload table make allImagesAlreadyNotForUpload false
|
||||
if (exists < 1) {
|
||||
allImagesAlreadyNotForUpload = false
|
||||
}
|
||||
}
|
||||
|
||||
// if all images is not already marked as not for upload, insert all images in
|
||||
// not for upload table
|
||||
if (!allImagesAlreadyNotForUpload) {
|
||||
images.forEach {
|
||||
// Insert or delete images as necessary, but the UI updates should be posted back to the main thread
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
it.uri,
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
notForUploadStatusDao.insert(
|
||||
NotForUploadStatus(
|
||||
imageSHA1
|
||||
)
|
||||
)
|
||||
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
images.forEach { image ->
|
||||
imageFragment?.removeImage(image)
|
||||
}
|
||||
imageFragment?.clearSelectedImages()
|
||||
}
|
||||
|
||||
// if all images is already marked as not for upload, delete all images from
|
||||
// not for upload table
|
||||
} else {
|
||||
images.forEach {
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
it.uri,
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
imageFragment?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
imageFragment!!.refresh()
|
||||
val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout)
|
||||
bottomLayout.visibility = View.GONE
|
||||
withContext(Dispatchers.Main) {
|
||||
imageFragment?.dismissMarkUnmarkProgressDialog()
|
||||
val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout)
|
||||
bottomLayout.visibility = View.GONE
|
||||
changeTitle(bucketName, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,10 +311,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
/**
|
||||
* Change the title of the toolbar.
|
||||
*/
|
||||
private fun changeTitle(title: String) {
|
||||
val titleText = findViewById<TextView>(R.id.title)
|
||||
if (titleText != null) {
|
||||
titleText.text = title
|
||||
private fun changeTitle(title: String, selectedImageCount:Int) {
|
||||
if (title.isNotEmpty()){
|
||||
val titleText = findViewById<TextView>(R.id.title)
|
||||
var titleWithAppendedImageCount = title
|
||||
if (selectedImageCount > 0) {
|
||||
titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix,
|
||||
selectedImageCount, selectedImageCount)})"
|
||||
}
|
||||
if (titleText != null) {
|
||||
titleText.text = titleWithAppendedImageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,6 +331,10 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
private fun setUpToolbar() {
|
||||
val back: ImageButton = findViewById(R.id.back)
|
||||
back.setOnClickListener { onBackPressed() }
|
||||
|
||||
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
||||
limitError.visibility = View.INVISIBLE
|
||||
limitError.setOnClickListener { displayUploadLimitWarning() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -308,7 +346,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
.addToBackStack(null)
|
||||
.commit()
|
||||
|
||||
changeTitle(folderName)
|
||||
changeTitle(folderName, 0)
|
||||
|
||||
bucketId = folderId
|
||||
bucketName = folderName
|
||||
|
|
@ -323,8 +361,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
selectedNotForUploadImages: Int
|
||||
) {
|
||||
viewModel.selectedImages.value = selectedImages
|
||||
changeTitle(bucketName, selectedImages.size)
|
||||
|
||||
if (selectedNotForUploadImages > 0) {
|
||||
uploadLimitExceeded = selectedImages.size > uploadLimit
|
||||
uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0)
|
||||
|
||||
if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
|
||||
toolbarBinding.imageLimitError.visibility = View.VISIBLE
|
||||
bottomSheetBinding.upload.text = resources.getString(
|
||||
R.string.custom_selector_button_limit_text, uploadLimit)
|
||||
} else {
|
||||
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
|
||||
bottomSheetBinding.upload.text = resources.getString(R.string.upload)
|
||||
}
|
||||
|
||||
if (uploadLimitExceeded || selectedNotForUploadImages > 0) {
|
||||
bottomSheetBinding.upload.isEnabled = false
|
||||
bottomSheetBinding.upload.alpha = 0.5f
|
||||
} else {
|
||||
|
|
@ -334,8 +385,14 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
|
||||
bottomSheetBinding.notForUpload.text =
|
||||
when (selectedImages.size == selectedNotForUploadImages) {
|
||||
true -> getString(R.string.unmark_as_not_for_upload)
|
||||
else -> getString(R.string.mark_as_not_for_upload)
|
||||
true -> {
|
||||
progressDialogText=getString(R.string.unmarking_as_not_for_upload)
|
||||
getString(R.string.unmark_as_not_for_upload)
|
||||
}
|
||||
else -> {
|
||||
progressDialogText=getString(R.string.marking_as_not_for_upload)
|
||||
getString(R.string.mark_as_not_for_upload)
|
||||
}
|
||||
}
|
||||
|
||||
val bottomLayout: ConstraintLayout = findViewById(R.id.bottom_layout)
|
||||
|
|
@ -366,22 +423,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
* Get the selected images. Remove any non existent file, forward the data to finish selector.
|
||||
*/
|
||||
fun onDone() {
|
||||
val selectedImages = viewModel.selectedImages.value
|
||||
if (selectedImages.isNullOrEmpty()) {
|
||||
finishPickImages(arrayListOf())
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
while (i < selectedImages.size) {
|
||||
val path = selectedImages[i].path
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
selectedImages.removeAt(i)
|
||||
i--
|
||||
val selectedImages = viewModel.selectedImages.value
|
||||
if (selectedImages.isNullOrEmpty()) {
|
||||
finishPickImages(arrayListOf())
|
||||
return
|
||||
}
|
||||
i++
|
||||
}
|
||||
finishPickImages(selectedImages)
|
||||
var i = 0
|
||||
while (i < selectedImages.size) {
|
||||
val path = selectedImages[i].path
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
selectedImages.removeAt(i)
|
||||
i--
|
||||
}
|
||||
i++
|
||||
}
|
||||
finishPickImages(selectedImages)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -404,10 +461,24 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
|
||||
if (fragment != null && fragment is FolderFragment) {
|
||||
isImageFragmentOpen = false
|
||||
changeTitle(getString(R.string.custom_selector_title))
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog explaining the upload limit warning.
|
||||
*/
|
||||
private fun displayUploadLimitWarning() {
|
||||
val dialog = Dialog(this)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
||||
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener()
|
||||
{ dialog.dismiss() }
|
||||
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString(
|
||||
R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* On activity destroy
|
||||
* If image fragment is open, overwrite its attributes otherwise discard the values.
|
||||
|
|
@ -426,4 +497,4 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
const val FOLDER_NAME: String = "FolderName"
|
||||
const val ITEM_ID: String = "ItemId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
*/
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||
folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener)
|
||||
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
|
||||
gridLayoutManager = GridLayoutManager(context, columnCount())
|
||||
selectorRV = binding?.selectorRv
|
||||
loader = binding?.loader
|
||||
|
|
@ -159,4 +159,4 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
return 2
|
||||
// todo change column count depending on the orientation of the device.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
|
|
@ -90,6 +92,12 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
}
|
||||
|
||||
if (file != null && file.exists() && name != null && path != null && bucketName != null) {
|
||||
val extension = path.substringAfterLast(".", "")
|
||||
// Check if the extension is one of the allowed types
|
||||
if (extension.lowercase(Locale.ROOT) !in arrayOf("jpg", "jpeg", "png", "svg", "gif", "tiff", "webp", "xcf")) {
|
||||
continue
|
||||
}
|
||||
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
|
|
@ -130,4 +138,4 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
* Sha1 for image (original image).
|
||||
*
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,37 +9,41 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Switch
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.ContributionDao
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||
import fr.free.nrw.commons.customselector.listeners.PassDataListener
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.listeners.PassDataListener
|
||||
import fr.free.nrw.commons.customselector.listeners.RefreshUIListener
|
||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
|
||||
import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
|
||||
import fr.free.nrw.commons.databinding.ProgressDialogBinding
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.FileProcessor
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Custom Selector Image Fragment.
|
||||
*/
|
||||
class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
||||
class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
||||
|
||||
private var _binding: FragmentCustomSelectorBinding? = null
|
||||
private val binding get() = _binding
|
||||
|
|
@ -57,7 +61,7 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
/**
|
||||
* View model for images.
|
||||
*/
|
||||
private var viewModel: CustomSelectorViewModel? = null
|
||||
private var viewModel: CustomSelectorViewModel? = null
|
||||
|
||||
/**
|
||||
* View Elements.
|
||||
|
|
@ -99,6 +103,10 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
*/
|
||||
private var progressLayout: ConstraintLayout? = null
|
||||
|
||||
private lateinit var progressDialog: AlertDialog
|
||||
private lateinit var progressDialogLayout: ProgressDialogBinding
|
||||
|
||||
|
||||
/**
|
||||
* NotForUploadStatus Dao class for database operations
|
||||
*/
|
||||
|
|
@ -129,6 +137,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
@Inject
|
||||
lateinit var mediaClient: MediaClient
|
||||
|
||||
@Inject
|
||||
lateinit var contributionDao: ContributionDao
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
|
@ -163,7 +174,9 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
super.onCreate(savedInstanceState)
|
||||
bucketId = arguments?.getLong(BUCKET_ID)
|
||||
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
||||
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java)
|
||||
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
||||
CustomSelectorViewModel::class.java
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,17 +184,22 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
* Init imageAdapter, gridLayoutManger.
|
||||
* SetUp recycler view.
|
||||
*/
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||
imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
||||
gridLayoutManager = GridLayoutManager(context,getSpanCount())
|
||||
with(binding?.selectorRv){
|
||||
imageAdapter =
|
||||
ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
||||
gridLayoutManager = GridLayoutManager(context, getSpanCount())
|
||||
with(binding?.selectorRv) {
|
||||
this?.layoutManager = gridLayoutManager
|
||||
this?.setHasFixedSize(true)
|
||||
this?.adapter = imageAdapter
|
||||
}
|
||||
|
||||
viewModel?.result?.observe(viewLifecycleOwner, Observer{
|
||||
viewModel?.result?.observe(viewLifecycleOwner, Observer {
|
||||
handleResult(it)
|
||||
})
|
||||
|
||||
|
|
@ -194,9 +212,16 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
|
||||
val sharedPreferences: SharedPreferences =
|
||||
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
|
||||
showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
showAlreadyActionedImages =
|
||||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
switch?.isChecked = showAlreadyActionedImages
|
||||
|
||||
val builder = AlertDialog.Builder(requireActivity())
|
||||
builder.setCancelable(false)
|
||||
progressDialogLayout = ProgressDialogBinding.inflate(layoutInflater, container, false)
|
||||
builder.setView(progressDialogLayout.root)
|
||||
progressDialog = builder.create()
|
||||
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +242,8 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
editor.apply()
|
||||
}
|
||||
|
||||
imageAdapter.init(allImages, allImages, TreeMap())
|
||||
val uploadingContributions = getUploadingContributions()
|
||||
imageAdapter.init(allImages, allImages, TreeMap(), uploadingContributions)
|
||||
imageAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
|
@ -236,13 +262,15 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
/**
|
||||
* Handle view model result.
|
||||
*/
|
||||
private fun handleResult(result:Result){
|
||||
if(result.status is CallbackStatus.SUCCESS){
|
||||
private fun handleResult(result: Result) {
|
||||
if (result.status is CallbackStatus.SUCCESS) {
|
||||
val images = result.images
|
||||
if(images.isNotEmpty()) {
|
||||
|
||||
val uploadingContributions = getUploadingContributions()
|
||||
if (images.isNotEmpty()) {
|
||||
filteredImages = ImageHelper.filterImages(images, bucketId)
|
||||
allImages = ArrayList(filteredImages)
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap())
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||
selectorRV?.let {
|
||||
it.visibility = View.VISIBLE
|
||||
lastItemId?.let { pos ->
|
||||
|
|
@ -250,18 +278,18 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
binding?.emptyText?.let {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
selectorRV?.let{
|
||||
selectorRV?.let {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
loader?.let {
|
||||
it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
|
||||
it.visibility =
|
||||
if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,19 +345,61 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassData
|
|||
}
|
||||
|
||||
override fun refresh() {
|
||||
imageAdapter.refresh(filteredImages, allImages)
|
||||
imageAdapter.refresh(filteredImages, allImages, getUploadingContributions())
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the image from the actionable image map
|
||||
*/
|
||||
fun removeImage(image : Image){
|
||||
imageAdapter.removeImageFromActionableImageMap(image)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the selected images
|
||||
*/
|
||||
fun clearSelectedImages() {
|
||||
imageAdapter.clearSelectedImages()
|
||||
}
|
||||
/**
|
||||
* Passes selected images and other information from Activity to Fragment and connects it with
|
||||
* the adapter
|
||||
*/
|
||||
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean){
|
||||
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) {
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
|
||||
val uploadingContributions = getUploadingContributions()
|
||||
|
||||
if (!showAlreadyActionedImages && shouldRefresh) {
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap())
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows mark/unmark progress dialog
|
||||
*/
|
||||
fun showMarkUnmarkProgressDialog(text: String) {
|
||||
if (!progressDialog.isShowing) {
|
||||
progressDialogLayout.progressDialogText.text = text
|
||||
progressDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses mark/unmark progress dialog
|
||||
*/
|
||||
fun dismissMarkUnmarkProgressDialog() {
|
||||
if (progressDialog.isShowing) {
|
||||
progressDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUploadingContributions(): List<Contribution> {
|
||||
|
||||
return contributionDao.getContribution(
|
||||
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED)
|
||||
)?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||
|
|
@ -75,7 +76,8 @@ class ImageLoader @Inject constructor(
|
|||
holder: ImageViewHolder,
|
||||
image: Image,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
uploadedContributionsList : List<Contribution>
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -84,6 +86,7 @@ class ImageLoader @Inject constructor(
|
|||
mapHolderImage[holder] = image
|
||||
holder.itemNotUploaded()
|
||||
holder.itemForUpload()
|
||||
holder.itemNotUploading()
|
||||
|
||||
scope.launch {
|
||||
var result: Result = Result.NOTFOUND
|
||||
|
|
@ -214,6 +217,17 @@ class ImageLoader @Inject constructor(
|
|||
holder.itemNotForUpload()
|
||||
} else holder.itemForUpload()
|
||||
}
|
||||
|
||||
if (uploadedContributionsList.isNotEmpty()) {
|
||||
for (contribution in uploadedContributionsList ) {
|
||||
if (contribution.contentUri == image.uri && showAlreadyActionedImages) {
|
||||
holder.itemUploading()
|
||||
break
|
||||
} else {
|
||||
holder.itemNotUploading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,17 +237,22 @@ class ImageLoader @Inject constructor(
|
|||
suspend fun nextActionableImage(
|
||||
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
nextImagePosition: Int
|
||||
nextImagePosition: Int,
|
||||
currentlyUploadingImages: List<Contribution>
|
||||
): Int {
|
||||
var next: Int
|
||||
|
||||
// Traversing from given position to the end
|
||||
for (i in nextImagePosition until allImages.size){
|
||||
val it = allImages[i]
|
||||
val imageSHA1: String = when (mapImageSHA1[it.uri] != null) {
|
||||
true -> mapImageSHA1[it.uri]!!
|
||||
val currentImage = allImages[i]
|
||||
|
||||
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
||||
continue // Skip this image as it's currently being uploaded
|
||||
}
|
||||
|
||||
val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) {
|
||||
true -> mapImageSHA1[currentImage.uri]!!
|
||||
else -> CustomSelectorUtils.getImageSHA1(
|
||||
it.uri,
|
||||
currentImage.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
context.contentResolver
|
||||
|
|
@ -253,7 +272,7 @@ class ImageLoader @Inject constructor(
|
|||
// If the image is not present in the already uploaded table, checks for its
|
||||
// modified SHA1 in already uploaded table
|
||||
if (next <= 0) {
|
||||
val modifiedImageSha1 = getSHA1(it, defaultDispatcher)
|
||||
val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher)
|
||||
next = uploadedStatusDao.findByModifiedImageSHA1(
|
||||
modifiedImageSha1,
|
||||
true
|
||||
|
|
@ -360,4 +379,4 @@ class ImageLoader @Inject constructor(
|
|||
const val INVALIDATE_DAY_COUNT: Long = 7
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import androidx.room.TypeConverters
|
|||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.ContributionDao
|
||||
import fr.free.nrw.commons.customselector.database.*
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.nearby.PlaceDao
|
||||
import fr.free.nrw.commons.review.ReviewDao
|
||||
import fr.free.nrw.commons.review.ReviewEntity
|
||||
import fr.free.nrw.commons.upload.depicts.Depicts
|
||||
|
|
@ -15,10 +17,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
|||
* The database for accessing the respective DAOs
|
||||
*
|
||||
*/
|
||||
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 15, exportSchema = false)
|
||||
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], version = 18, exportSchema = false)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun contributionDao(): ContributionDao
|
||||
abstract fun PlaceDao(): PlaceDao
|
||||
abstract fun DepictsDao(): DepictsDao;
|
||||
abstract fun UploadedStatusDao(): UploadedStatusDao;
|
||||
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import fr.free.nrw.commons.CommonsApplication;
|
|||
import fr.free.nrw.commons.contributions.ChunkInfo;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Sitelinks;
|
||||
import fr.free.nrw.commons.upload.WikidataPlace;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -134,6 +136,18 @@ public class Converters {
|
|||
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Sitelinks sitelinksFromString(String value) {
|
||||
Type type = new TypeToken<Sitelinks>() {}.getType();
|
||||
return new Gson().fromJson(value, type);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String fromSitelinks(Sitelinks sitelinks) {
|
||||
Gson gson = new Gson();
|
||||
return gson.toJson(sitelinks);
|
||||
}
|
||||
|
||||
private static String writeObjectToString(Object object) {
|
||||
return object == null ? null : getGson().toJson(object);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import fr.free.nrw.commons.BuildConfig;
|
|||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
|
|
@ -66,7 +67,13 @@ public class DeleteHelper {
|
|||
|
||||
return delete(media, reason)
|
||||
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
||||
.firstOrError();
|
||||
.firstOrError()
|
||||
.onErrorResumeNext(throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
return Single.error(throwable);
|
||||
}
|
||||
return Single.error(throwable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,22 +111,30 @@ public class DeleteHelper {
|
|||
}
|
||||
|
||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||
}
|
||||
throw new RuntimeException("Failed to nominate for deletion");
|
||||
}).flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||
}
|
||||
throw new RuntimeException("Failed to nominate for deletion");
|
||||
}).flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
|
||||
}
|
||||
throw new RuntimeException("Failed to nominate for deletion");
|
||||
});
|
||||
.onErrorResumeNext(throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
return Observable.error(throwable);
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean showDeletionNotification(Context context, Media media, boolean result) {
|
||||
|
|
@ -205,6 +220,8 @@ public class DeleteHelper {
|
|||
});
|
||||
|
||||
alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> {
|
||||
reviewCallback.disableButtons();
|
||||
|
||||
|
||||
String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " ";
|
||||
|
||||
|
|
@ -224,13 +241,15 @@ public class DeleteHelper {
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(aBoolean -> {
|
||||
if (aBoolean) {
|
||||
reviewCallback.onSuccess();
|
||||
reviewCallback.onSuccess();
|
||||
}, throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
reviewCallback.onTokenException((InvalidLoginTokenException) throwable);
|
||||
} else {
|
||||
reviewCallback.onFailure();
|
||||
}
|
||||
reviewCallback.enableButtons();
|
||||
});
|
||||
|
||||
});
|
||||
alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure());
|
||||
d = alert.create();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package fr.free.nrw.commons.delete;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import org.wikipedia.util.DateUtil;
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package fr.free.nrw.commons.description
|
||||
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.speech.RecognizerIntent
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding
|
||||
import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION
|
||||
import fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT
|
||||
import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
|
|
@ -18,8 +23,13 @@ import fr.free.nrw.commons.theme.BaseActivity
|
|||
import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Activity for populating and editing existing description and caption
|
||||
*/
|
||||
|
|
@ -40,6 +50,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
*/
|
||||
var wikiText: String? = null
|
||||
|
||||
/**
|
||||
* Media object
|
||||
*/
|
||||
var media: Media? = null
|
||||
|
||||
/**
|
||||
* Saved language
|
||||
*/
|
||||
|
|
@ -55,6 +70,15 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
|
||||
private lateinit var binding: ActivityDescriptionEditBinding
|
||||
|
||||
private val REQUEST_CODE_FOR_VOICE_INPUT = 1213
|
||||
|
||||
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||
|
||||
@Inject lateinit var descriptionEditHelper: DescriptionEditHelper
|
||||
|
||||
@Inject lateinit var sessionManager: SessionManager
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -62,13 +86,21 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
setContentView(binding.root)
|
||||
|
||||
val bundle = intent.extras
|
||||
val descriptionAndCaptions: ArrayList<UploadMediaDetail> =
|
||||
bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!!
|
||||
wikiText = bundle.getString(WIKITEXT)
|
||||
savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
descriptionAndCaptions = savedInstanceState.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)
|
||||
wikiText = savedInstanceState.getString(WIKITEXT)
|
||||
savedLanguageValue = savedInstanceState.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
||||
media = savedInstanceState.getParcelable("media")
|
||||
} else {
|
||||
descriptionAndCaptions =
|
||||
bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!!
|
||||
wikiText = bundle.getString(WIKITEXT)
|
||||
savedLanguageValue = bundle.getString(Prefs.DESCRIPTION_LANGUAGE)!!
|
||||
media = bundle.getParcelable("media")
|
||||
}
|
||||
initRecyclerView(descriptionAndCaptions)
|
||||
|
||||
binding.btnAddDescription.setOnClickListener(::onButtonAddDescriptionClicked)
|
||||
binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked)
|
||||
binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked)
|
||||
}
|
||||
|
|
@ -78,7 +110,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
* @param descriptionAndCaptions list of description and caption
|
||||
*/
|
||||
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(
|
||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(this,
|
||||
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
|
||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||
showInfoAlert(
|
||||
|
|
@ -107,17 +139,20 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
|
||||
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {}
|
||||
|
||||
private fun onBackButtonClicked(view: View) {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
private fun onButtonAddDescriptionClicked(view: View) {
|
||||
/**
|
||||
* Adds new language item to RecyclerView
|
||||
*/
|
||||
override fun addLanguage() {
|
||||
val uploadMediaDetail = UploadMediaDetail()
|
||||
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
|
||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
||||
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
||||
}
|
||||
|
||||
private fun onBackButtonClicked(view: View) {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
private fun onSubmitButtonClicked(view: View) {
|
||||
showLoggingProgressBar()
|
||||
val uploadMediaDetails = uploadMediaDetailAdapter.items
|
||||
|
|
@ -151,22 +186,85 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
buffer.append(uploadDetails.languageCode)
|
||||
buffer.append("|1=")
|
||||
buffer.append(uploadDetails.descriptionText)
|
||||
buffer.append("}}, ")
|
||||
buffer.append("}}")
|
||||
}
|
||||
}
|
||||
buffer.replace(", $".toRegex(), "")
|
||||
buffer.append(descriptionEnd)
|
||||
}
|
||||
val returningIntent = Intent()
|
||||
returningIntent.putExtra(UPDATED_WIKITEXT, buffer.toString())
|
||||
returningIntent.putParcelableArrayListExtra(
|
||||
LIST_OF_DESCRIPTION_AND_CAPTION,
|
||||
uploadMediaDetails as ArrayList<out Parcelable?>
|
||||
)
|
||||
setResult(RESULT_OK, returningIntent)
|
||||
editDescription(media!!, buffer.toString(), uploadMediaDetails as ArrayList<UploadMediaDetail>)
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits description and caption
|
||||
* @param media media object
|
||||
* @param updatedWikiText updated wiki text
|
||||
* @param uploadMediaDetails descriptions and captions
|
||||
*/
|
||||
private fun editDescription(media : Media, updatedWikiText : String, uploadMediaDetails : ArrayList<UploadMediaDetail>){
|
||||
|
||||
try {
|
||||
descriptionEditHelper?.addDescription(
|
||||
applicationContext, media,
|
||||
updatedWikiText
|
||||
)
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })?.let {
|
||||
compositeDisposable.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
} catch (e : InvalidLoginTokenException) {
|
||||
val username: String? = sessionManager?.userName
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
if (commonsApplication != null ){
|
||||
commonsApplication.clearApplicationData(this,logoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val updatedCaptions = LinkedHashMap<String, String>()
|
||||
for (mediaDetail in uploadMediaDetails) {
|
||||
try {
|
||||
compositeDisposable.add(
|
||||
descriptionEditHelper!!.addCaption(
|
||||
applicationContext, media,
|
||||
mediaDetail.languageCode, mediaDetail.captionText
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { s: Boolean? ->
|
||||
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
|
||||
media.captions = updatedCaptions
|
||||
Timber.d("Caption is added.")
|
||||
})
|
||||
}
|
||||
catch (e : InvalidLoginTokenException) {
|
||||
val username = sessionManager.userName
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
if (commonsApplication != null ){
|
||||
commonsApplication.clearApplicationData(this,logoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoggingProgressBar() {
|
||||
progressDialog = ProgressDialog(this)
|
||||
progressDialog!!.isIndeterminate = true
|
||||
|
|
@ -175,4 +273,24 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
progressDialog!!.setCanceledOnTouchOutside(false)
|
||||
progressDialog!!.show()
|
||||
}
|
||||
}
|
||||
|
||||
override
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS )
|
||||
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) }
|
||||
else { Timber.e("Error %s", resultCode) }
|
||||
}
|
||||
}
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
|
||||
outState.putString(WIKITEXT, wikiText)
|
||||
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
|
||||
//save Media
|
||||
outState.putParcelable("media", media)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import fr.free.nrw.commons.description.DescriptionEditActivity;
|
|||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.explore.SearchActivity;
|
||||
import fr.free.nrw.commons.media.ZoomableActivity;
|
||||
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.review.ReviewActivity;
|
||||
|
|
@ -79,4 +80,7 @@ public abstract class ActivityBuilderModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract ZoomableActivity bindZoomableActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WikidataFeedback bindWikiFeedback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package fr.free.nrw.commons.di;
|
|||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.explore.categories.CategoriesModule;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||
import fr.free.nrw.commons.navtab.NavTabLayout;
|
||||
import fr.free.nrw.commons.nearby.NearbyController;
|
||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
|
|
@ -69,6 +70,9 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
|||
|
||||
void inject(PicOfDayAppWidget picOfDayAppWidget);
|
||||
|
||||
@Singleton
|
||||
void inject(NearbyController nearbyController);
|
||||
|
||||
Gson gson();
|
||||
|
||||
@Component.Builder
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import fr.free.nrw.commons.data.DBOpenHelper;
|
|||
import fr.free.nrw.commons.db.AppDatabase;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.nearby.PlaceDao;
|
||||
import fr.free.nrw.commons.review.ReviewDao;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.UploadController;
|
||||
|
|
@ -41,7 +42,6 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.wikipedia.AppAdapter;
|
||||
|
||||
/**
|
||||
* The Dependency Provider class for Commons Android.
|
||||
|
|
@ -257,8 +257,8 @@ public class CommonsApplicationModule {
|
|||
|
||||
@Named("username")
|
||||
@Provides
|
||||
public String provideLoggedInUsername() {
|
||||
return Objects.toString(AppAdapter.get().getUserName(), "");
|
||||
public String provideLoggedInUsername(SessionManager sessionManager) {
|
||||
return Objects.toString(sessionManager.getUserName(), "");
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -276,6 +276,11 @@ public class CommonsApplicationModule {
|
|||
return appDatabase.contributionDao();
|
||||
}
|
||||
|
||||
@Provides
|
||||
public PlaceDao providesPlaceDao(AppDatabase appDatabase) {
|
||||
return appDatabase.PlaceDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference of DepictsDao class.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,8 +7,16 @@ import dagger.Module;
|
|||
import dagger.Provides;
|
||||
import fr.free.nrw.commons.BetaConstants;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.OkHttpConnectionFactory;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.actions.PageEditInterface;
|
||||
import fr.free.nrw.commons.actions.ThanksInterface;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface;
|
||||
import fr.free.nrw.commons.auth.csrf.LogoutClient;
|
||||
import fr.free.nrw.commons.auth.login.LoginClient;
|
||||
import fr.free.nrw.commons.auth.login.LoginInterface;
|
||||
import fr.free.nrw.commons.category.CategoryInterface;
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
|
|
@ -18,11 +26,15 @@ import fr.free.nrw.commons.media.PageMediaInterface;
|
|||
import fr.free.nrw.commons.media.WikidataMediaInterface;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.mwapi.UserInterface;
|
||||
import fr.free.nrw.commons.notification.NotificationInterface;
|
||||
import fr.free.nrw.commons.review.ReviewInterface;
|
||||
import fr.free.nrw.commons.upload.UploadInterface;
|
||||
import fr.free.nrw.commons.upload.WikiBaseInterface;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
||||
import fr.free.nrw.commons.wikidata.CommonsServiceFactory;
|
||||
import fr.free.nrw.commons.wikidata.WikidataInterface;
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage;
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -33,31 +45,25 @@ import okhttp3.HttpUrl;
|
|||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.json.GsonUtil;
|
||||
import org.wikipedia.login.LoginClient;
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
||||
import fr.free.nrw.commons.wikidata.GsonUtil;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class NetworkingModule {
|
||||
private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql";
|
||||
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/urbanecmbot/commonsmisc";
|
||||
|
||||
private static final String TEST_TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app";
|
||||
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app";
|
||||
|
||||
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
|
||||
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
|
||||
private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite";
|
||||
|
||||
public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite";
|
||||
|
||||
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
|
||||
public static final String NAMED_WIKI_CSRF = "wiki-csrf";
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
@ -73,6 +79,12 @@ public class NetworkingModule {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) {
|
||||
return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public HttpLoggingInterceptor provideHttpLoggingInterceptor() {
|
||||
|
|
@ -88,29 +100,86 @@ public class NetworkingModule {
|
|||
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
|
||||
DepictsClient depictsClient,
|
||||
@Named("tools_forge") HttpUrl toolsForgeUrl,
|
||||
@Named("test_tools_forge") HttpUrl testToolsForgeUrl,
|
||||
@Named("default_preferences") JsonKvStore defaultKvStore,
|
||||
Gson gson) {
|
||||
return new OkHttpJsonApiClient(okHttpClient,
|
||||
depictsClient,
|
||||
toolsForgeUrl,
|
||||
testToolsForgeUrl,
|
||||
WIKIDATA_SPARQL_QUERY_URL,
|
||||
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
|
||||
gson);
|
||||
}
|
||||
|
||||
@Named(NAMED_COMMONS_CSRF)
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return new CsrfTokenClient(commonsWikiSite, commonsWikiSite);
|
||||
public CommonsCookieStorage provideCookieStorage(
|
||||
@Named("default_preferences") JsonKvStore preferences) {
|
||||
CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences);
|
||||
cookieStorage.load();
|
||||
return cookieStorage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LoginClient provideLoginClient() {
|
||||
return new LoginClient();
|
||||
public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) {
|
||||
return new CommonsCookieJar(storage);
|
||||
}
|
||||
|
||||
@Named(NAMED_COMMONS_CSRF)
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager,
|
||||
@Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
|
||||
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a singleton instance of CsrfTokenClient for Wikidata.
|
||||
*
|
||||
* @param sessionManager The session manager to manage user sessions.
|
||||
* @param tokenInterface The interface for obtaining CSRF tokens.
|
||||
* @param loginClient The client for handling login operations.
|
||||
* @param logoutClient The client for handling logout operations.
|
||||
* @return A singleton instance of CsrfTokenClient.
|
||||
*/
|
||||
@Named(NAMED_WIKI_CSRF)
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager,
|
||||
@Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
|
||||
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a singleton instance of CsrfTokenInterface for Wikidata.
|
||||
*
|
||||
* @param serviceFactory The factory used to create service interfaces.
|
||||
* @return A singleton instance of CsrfTokenInterface for Wikidata.
|
||||
*/
|
||||
@Named("wikidata-csrf-interface")
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class);
|
||||
}
|
||||
|
||||
@Named("commons-csrf-interface")
|
||||
@Provides
|
||||
@Singleton
|
||||
public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LoginClient provideLoginClient(LoginInterface loginInterface) {
|
||||
return new LoginClient(loginInterface);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -129,21 +198,6 @@ public class NetworkingModule {
|
|||
return HttpUrl.parse(TOOLS_FORGE_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("test_tools_forge")
|
||||
@NonNull
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public HttpUrl provideTestToolsForgeUrl() {
|
||||
return HttpUrl.parse(TEST_TOOLS_FORGE_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
||||
public WikiSite provideCommonsWikiSite() {
|
||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NAMED_WIKI_DATA_WIKI_SITE)
|
||||
|
|
@ -164,54 +218,40 @@ public class NetworkingModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("commons-service")
|
||||
public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite);
|
||||
public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("wikidata-service")
|
||||
public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class);
|
||||
public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
|
||||
public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikiBaseInterface provideWikiBaseInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, WikiBaseInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class);
|
||||
public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class);
|
||||
}
|
||||
|
||||
@Named("commons-page-edit-service")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class);
|
||||
public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class);
|
||||
}
|
||||
|
||||
@Named("wikidata-page-edit-service")
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
|
||||
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class);
|
||||
public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class);
|
||||
}
|
||||
|
||||
@Named("commons-page-edit")
|
||||
|
|
@ -222,10 +262,25 @@ public class NetworkingModule {
|
|||
return new PageEditClient(csrfTokenClient, pageEditInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a singleton instance of PageEditClient for Wikidata.
|
||||
*
|
||||
* @param csrfTokenClient The client used to manage CSRF tokens.
|
||||
* @param pageEditInterface The interface for page edit operations.
|
||||
* @return A singleton instance of PageEditClient for Wikidata.
|
||||
*/
|
||||
@Named("wikidata-page-edit")
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
|
||||
public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient,
|
||||
@Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) {
|
||||
return new PageEditClient(csrfTokenClient, pageEditInterface);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -236,36 +291,44 @@ public class NetworkingModule {
|
|||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikidataMediaInterface provideWikidataMediaInterface(
|
||||
@Named(NAMED_COMMONS_WIKI_SITE) final WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite,
|
||||
BetaConstants.COMMONS_URL, WikidataMediaInterface.class);
|
||||
public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) {
|
||||
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
|
||||
public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CategoryInterface provideCategoryInterface(
|
||||
@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory
|
||||
.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
|
||||
public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UserInterface.class);
|
||||
public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikidataInterface provideWikidataInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
|
||||
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, WikidataInterface.class);
|
||||
public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -274,8 +337,8 @@ public class NetworkingModule {
|
|||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite) {
|
||||
return ServiceFactory.get(wikiSite, wikiSite.url(), PageMediaInterface.class);
|
||||
public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) {
|
||||
return serviceFactory.create(wikiSite.url(), PageMediaInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
301
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
301
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
package fr.free.nrw.commons.edit
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.Animator.AnimatorListener
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.media.ExifInterface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.rotationMatrix
|
||||
import androidx.core.graphics.scaleMatrix
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import fr.free.nrw.commons.databinding.ActivityEditBinding
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* An activity class for editing and rotating images using LLJTran with EXIF attribute preservation.
|
||||
*
|
||||
* This activity allows loads an image, allows users to rotate it by 90-degree increments, and
|
||||
* save the edited image while preserving its EXIF attributes. The class includes methods
|
||||
* for initializing the UI, animating image rotations, copying EXIF data, and handling
|
||||
* the image-saving process.
|
||||
*/
|
||||
class EditActivity : AppCompatActivity() {
|
||||
private var imageUri = ""
|
||||
private lateinit var vm: EditViewModel
|
||||
private val sourceExifAttributeList = mutableListOf<Pair<String, String?>>()
|
||||
private lateinit var binding: ActivityEditBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityEditBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.title = ""
|
||||
val intent = intent
|
||||
imageUri = intent.getStringExtra("image") ?: ""
|
||||
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
||||
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
||||
val exifTags = arrayOf(
|
||||
ExifInterface.TAG_APERTURE,
|
||||
ExifInterface.TAG_DATETIME,
|
||||
ExifInterface.TAG_EXPOSURE_TIME,
|
||||
ExifInterface.TAG_FLASH,
|
||||
ExifInterface.TAG_FOCAL_LENGTH,
|
||||
ExifInterface.TAG_GPS_ALTITUDE,
|
||||
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||
ExifInterface.TAG_GPS_DATESTAMP,
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_ISO,
|
||||
ExifInterface.TAG_MAKE,
|
||||
ExifInterface.TAG_MODEL,
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.TAG_WHITE_BALANCE,
|
||||
ExifInterface.WHITEBALANCE_AUTO,
|
||||
ExifInterface.WHITEBALANCE_MANUAL
|
||||
)
|
||||
for (tag in exifTags) {
|
||||
val attribute = sourceExif?.getAttribute(tag.toString())
|
||||
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
||||
}
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ImageView and associated UI elements.
|
||||
*
|
||||
* This function sets up the ImageView for displaying an image, adjusts its view bounds,
|
||||
* and scales the initial image to fit within the ImageView. It also sets click listeners
|
||||
* for the "Rotate" and "Save" buttons.
|
||||
*/
|
||||
private fun init() {
|
||||
binding.iv.adjustViewBounds = true
|
||||
binding.iv.scaleType = ImageView.ScaleType.MATRIX
|
||||
binding.iv.post(Runnable {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(imageUri, options)
|
||||
|
||||
val bitmapWidth = options.outWidth
|
||||
val bitmapHeight = options.outHeight
|
||||
|
||||
// Check if the bitmap dimensions exceed a certain threshold
|
||||
val maxBitmapSize = 2000 // Set your maximum size here
|
||||
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
||||
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
||||
options.inSampleSize = scaleFactor
|
||||
options.inJustDecodeBounds = false
|
||||
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(scaledBitmap)
|
||||
// Update the ImageView with the scaled bitmap
|
||||
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
} else {
|
||||
|
||||
options.inJustDecodeBounds = false
|
||||
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(bitmap)
|
||||
|
||||
val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
}
|
||||
})
|
||||
binding.rotateBtn.setOnClickListener {
|
||||
animateImageHeight()
|
||||
}
|
||||
binding.btnSave.setOnClickListener {
|
||||
getRotatedImage()
|
||||
}
|
||||
}
|
||||
|
||||
var imageRotation = 0
|
||||
|
||||
/**
|
||||
* Animates the height, rotation, and scale of an ImageView to provide a smooth
|
||||
* transition effect when rotating an image by 90 degrees.
|
||||
*
|
||||
* This function calculates the new height, rotation, and scale for the ImageView
|
||||
* based on the current image rotation angle and animates the changes using a
|
||||
* ValueAnimator. It also disables a rotate button during the animation to prevent
|
||||
* further rotation actions.
|
||||
*/
|
||||
private fun animateImageHeight() {
|
||||
val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat()
|
||||
val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat()
|
||||
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
|
||||
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
|
||||
val rotation = imageRotation % 360
|
||||
val newRotation = rotation + 90
|
||||
|
||||
val newViewHeight: Int
|
||||
val imageScale: Float
|
||||
val newImageScale: Float
|
||||
|
||||
Timber.d("Rotation $rotation")
|
||||
Timber.d("new Rotation $newRotation")
|
||||
|
||||
|
||||
if (rotation == 0 || rotation == 180) {
|
||||
imageScale = viewWidth / drawableWidth
|
||||
newImageScale = viewWidth / drawableHeight
|
||||
newViewHeight = (drawableWidth * newImageScale).toInt()
|
||||
} else if (rotation == 90 || rotation == 270) {
|
||||
imageScale = viewWidth / drawableHeight
|
||||
newImageScale = viewWidth / drawableWidth
|
||||
newViewHeight = (drawableHeight * newImageScale).toInt()
|
||||
} else {
|
||||
throw UnsupportedOperationException("rotation can 0, 90, 180 or 270. \${rotation} is unsupported")
|
||||
}
|
||||
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L)
|
||||
|
||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
animator.addListener(object : AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
binding.rotateBtn.setEnabled(false)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
imageRotation = newRotation % 360
|
||||
binding.rotateBtn.setEnabled(true)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
animator.addUpdateListener { animation ->
|
||||
val animVal = animation.animatedValue as Float
|
||||
val complementaryAnimVal = 1 - animVal
|
||||
val animatedHeight =
|
||||
(complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt()
|
||||
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
||||
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
||||
binding.iv.getLayoutParams().height = animatedHeight
|
||||
val matrix: Matrix = rotationMatrix(
|
||||
animatedRotation,
|
||||
drawableWidth / 2,
|
||||
drawableHeight / 2
|
||||
)
|
||||
matrix.postScale(
|
||||
animatedScale,
|
||||
animatedScale,
|
||||
drawableWidth / 2,
|
||||
drawableHeight / 2
|
||||
)
|
||||
matrix.postTranslate(
|
||||
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
|
||||
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2
|
||||
)
|
||||
binding.iv.setImageMatrix(matrix)
|
||||
binding.iv.requestLayout()
|
||||
}
|
||||
|
||||
animator.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates and edits the current image, copies EXIF data, and returns the edited image path.
|
||||
*
|
||||
* This function retrieves the path of the current image specified by `imageUri`,
|
||||
* rotates it based on the `imageRotation` angle using the `rotateImage` method
|
||||
* from the `vm`, and updates the EXIF attributes of the
|
||||
* rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data
|
||||
* using the `copyExifData` method, creates an Intent to return the edited image's file path
|
||||
* as a result, and finishes the current activity.
|
||||
*/
|
||||
fun getRotatedImage() {
|
||||
|
||||
val filePath = imageUri.toUri().path
|
||||
val file = filePath?.let { File(it) }
|
||||
|
||||
|
||||
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
||||
if (rotatedImage == null) {
|
||||
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val editedImageExif: ExifInterface?
|
||||
if (rotatedImage?.path != null) {
|
||||
editedImageExif = ExifInterface(rotatedImage.path)
|
||||
copyExifData(editedImageExif)
|
||||
}
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object.
|
||||
*
|
||||
* This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes
|
||||
* on the provided `editedImageExif` object.
|
||||
*
|
||||
* @param editedImageExif The ExifInterface object for the edited image.
|
||||
*/
|
||||
private fun copyExifData(editedImageExif: ExifInterface?) {
|
||||
|
||||
for (attr in sourceExifAttributeList) {
|
||||
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||
editedImageExif!!.setAttribute(attr.first, attr.second)
|
||||
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||
}
|
||||
|
||||
editedImageExif?.saveAttributes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scale factor to be used for scaling down a bitmap based on its original
|
||||
* dimensions and the maximum allowed size.
|
||||
* @param originalWidth The original width of the bitmap.
|
||||
* @param originalHeight The original height of the bitmap.
|
||||
* @param maxSize The maximum allowed size for either width or height.
|
||||
* @return The scale factor to be used for scaling down the bitmap.
|
||||
* If the bitmap is smaller than or equal to the maximum size in both dimensions,
|
||||
* the scale factor is 1.
|
||||
* If the bitmap is larger than the maximum size in either dimension,
|
||||
* the scale factor is calculated as the largest power of 2 that is less than or equal
|
||||
* to the ratio of the original dimension to the maximum size.
|
||||
* The scale factor ensures that the scaled bitmap will fit within the maximum size
|
||||
* while maintaining aspect ratio.
|
||||
*/
|
||||
private fun calculateScaleFactor(originalWidth: Int, originalHeight: Int, maxSize: Int): Int {
|
||||
var scaleFactor = 1
|
||||
|
||||
if (originalWidth > maxSize || originalHeight > maxSize) {
|
||||
// Calculate the largest power of 2 that is less than or equal to the desired width and height
|
||||
val widthRatio = Math.ceil((originalWidth.toDouble() / maxSize.toDouble())).toInt()
|
||||
val heightRatio = Math.ceil((originalHeight.toDouble() / maxSize.toDouble())).toInt()
|
||||
|
||||
scaleFactor = if (widthRatio > heightRatio) widthRatio else heightRatio
|
||||
}
|
||||
|
||||
return scaleFactor
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package fr.free.nrw.commons.edit
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* ViewModel for image editing operations.
|
||||
*
|
||||
* This ViewModel class is responsible for managing image editing operations, such as
|
||||
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
||||
*/
|
||||
class EditViewModel() : ViewModel() {
|
||||
|
||||
// Ideally should be injected using DI
|
||||
private val transformImage: TransformImage = TransformImageImpl()
|
||||
|
||||
/**
|
||||
* Rotates the specified image file by the given degree.
|
||||
*
|
||||
* @param degree The degree by which to rotate the image.
|
||||
* @param imageFile The File representing the image to be rotated.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
fun rotateImage(degree: Int, imageFile: File): File? {
|
||||
return transformImage.rotateImage(imageFile, degree)
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package fr.free.nrw.commons.edit
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Interface for image transformation operations.
|
||||
*
|
||||
* This interface defines a contract for image transformation operations, allowing
|
||||
* implementations to provide specific functionality for tasks like rotating images.
|
||||
*/
|
||||
interface TransformImage {
|
||||
|
||||
/**
|
||||
* Rotates the specified image file by the given degree.
|
||||
*
|
||||
* @param imageFile The File representing the image to be rotated.
|
||||
* @param degree The degree by which to rotate the image.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
fun rotateImage(imageFile: File, degree : Int ):File?
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package fr.free.nrw.commons.edit
|
||||
|
||||
import android.mediautil.image.jpeg.LLJTran
|
||||
import android.mediautil.image.jpeg.LLJTranException
|
||||
import android.os.Environment
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* Implementation of the TransformImage interface for image rotation operations.
|
||||
*
|
||||
* This class provides an implementation for the TransformImage interface, right now it exposes a
|
||||
* function for rotating images by a specified degree using the LLJTran library. Right now it reads
|
||||
* the input image file, performs the rotation, and saves the rotated image to a new file.
|
||||
*/
|
||||
class TransformImageImpl() : TransformImage {
|
||||
|
||||
/**
|
||||
* Rotates the specified image file by the given degree.
|
||||
*
|
||||
* @param imageFile The File representing the image to be rotated.
|
||||
* @param degree The degree by which to rotate the image.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
override fun rotateImage(imageFile: File, degree : Int): File? {
|
||||
|
||||
Timber.tag("Trying to rotate image").d("Starting")
|
||||
|
||||
val path = Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
)
|
||||
|
||||
val imagePath = System.currentTimeMillis()
|
||||
val file: File = File(path, "$imagePath.jpg")
|
||||
|
||||
val output = file
|
||||
|
||||
val rotated = try {
|
||||
val lljTran = LLJTran(imageFile)
|
||||
lljTran.read(
|
||||
LLJTran.READ_ALL,
|
||||
false,
|
||||
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||
lljTran.transform(
|
||||
when(degree){
|
||||
90 -> LLJTran.ROT_90
|
||||
180 -> LLJTran.ROT_180
|
||||
270 -> LLJTran.ROT_270
|
||||
else -> {
|
||||
LLJTran.ROT_90
|
||||
}
|
||||
},
|
||||
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
|
||||
)
|
||||
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
||||
lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
|
||||
}
|
||||
lljTran.freeMemory()
|
||||
true
|
||||
} catch (e: LLJTranException) {
|
||||
Timber.tag("Error").d(e)
|
||||
return null
|
||||
false
|
||||
}
|
||||
|
||||
if (rotated) {
|
||||
Timber.tag("Done rotating image").d("Done")
|
||||
Timber.tag("Add").d(output.absolutePath)
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentExploreBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
|
|
@ -33,10 +32,8 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
private static final String EXPLORE_MAP = "Map";
|
||||
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
|
||||
|
||||
@BindView(R.id.tab_layout)
|
||||
TabLayout tabLayout;
|
||||
@BindView(R.id.viewPager)
|
||||
ParentViewPager viewPager;
|
||||
|
||||
public FragmentExploreBinding binding;
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
private ExploreListRootFragment featuredRootFragment;
|
||||
private ExploreListRootFragment mobileRootFragment;
|
||||
|
|
@ -46,7 +43,10 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
public JsonKvStore applicationKvStore;
|
||||
|
||||
public void setScroll(boolean canScroll){
|
||||
viewPager.setCanScroll(canScroll);
|
||||
if (binding != null)
|
||||
{
|
||||
binding.viewPager.setCanScroll(canScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -56,22 +56,17 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_explore, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
binding = FragmentExploreBinding.inflate(inflater, container, false);
|
||||
|
||||
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setId(R.id.viewPager);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
viewPager.addOnPageChangeListener(new OnPageChangeListener() {
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.viewPager.setId(R.id.viewPager);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset,
|
||||
int positionOffsetPixels) {
|
||||
|
|
@ -81,9 +76,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (position == 2) {
|
||||
viewPager.setCanScroll(false);
|
||||
binding.viewPager.setCanScroll(false);
|
||||
} else {
|
||||
viewPager.setCanScroll(true);
|
||||
binding.viewPager.setCanScroll(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +89,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
});
|
||||
setTabs();
|
||||
setHasOptionsMenu(true);
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,13 +128,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
if (tabLayout.getSelectedTabPosition() == 0) {
|
||||
if (binding.tabLayout.getSelectedTabPosition() == 0) {
|
||||
if (featuredRootFragment.backPressed()) {
|
||||
((BaseActivity) getActivity()).getSupportActionBar()
|
||||
.setDisplayHomeAsUpEnabled(false);
|
||||
return true;
|
||||
}
|
||||
} else if (tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
|
||||
} else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
|
||||
if (mobileRootFragment.backPressed()) {
|
||||
((BaseActivity) getActivity()).getSupportActionBar()
|
||||
.setDisplayHomeAsUpEnabled(false);
|
||||
|
|
@ -180,6 +175,12 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
|
|||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import android.widget.FrameLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
|
|
@ -26,8 +25,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
|||
private MediaDetailPagerFragment mediaDetails;
|
||||
private CategoriesMediaFragment listFragment;
|
||||
|
||||
@BindView(R.id.explore_container)
|
||||
FrameLayout container;
|
||||
private FragmentFeaturedRootBinding binding;
|
||||
|
||||
public ExploreListRootFragment() {
|
||||
//empty constructor necessary otherwise crashes on recreate
|
||||
|
|
@ -47,9 +45,9 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
|||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
return view;
|
||||
|
||||
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -109,9 +107,13 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
|||
|
||||
@Override
|
||||
public void onMediaClicked(int position) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
if (binding!=null) {
|
||||
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (((ExploreFragment) getParentFragment()).binding!=null) {
|
||||
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
((ExploreFragment) getParentFragment()).setScroll(false);
|
||||
setFragment(mediaDetails, listFragment);
|
||||
mediaDetails.showImage(position);
|
||||
|
|
@ -185,16 +187,29 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
|
|||
*/
|
||||
public boolean backPressed() {
|
||||
if (null != mediaDetails && mediaDetails.isVisible()) {
|
||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
|
||||
if (((ExploreFragment) getParentFragment()).binding != null) {
|
||||
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
removeFragment(mediaDetails);
|
||||
((ExploreFragment) getParentFragment()).setScroll(true);
|
||||
setFragment(listFragment, mediaDetails);
|
||||
((MainActivity) getActivity()).showTabs();
|
||||
return true;
|
||||
} else {
|
||||
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
||||
if (((MainActivity) getActivity()) != null) {
|
||||
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
||||
}
|
||||
}
|
||||
if (((MainActivity) getActivity()) != null) {
|
||||
((MainActivity) getActivity()).showTabs();
|
||||
}
|
||||
((MainActivity) getActivity()).showTabs();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import android.widget.FrameLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
|
|
@ -26,8 +25,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
|||
private MediaDetailPagerFragment mediaDetails;
|
||||
private ExploreMapFragment mapFragment;
|
||||
|
||||
@BindView(R.id.explore_container)
|
||||
FrameLayout container;
|
||||
private FragmentFeaturedRootBinding binding;
|
||||
|
||||
public ExploreMapRootFragment() {
|
||||
//empty constructor necessary otherwise crashes on recreate
|
||||
|
|
@ -54,9 +52,10 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
|||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
return view;
|
||||
|
||||
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -116,9 +115,9 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
|||
|
||||
@Override
|
||||
public void onMediaClicked(int position) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
binding.exploreContainer.setVisibility(View.VISIBLE);
|
||||
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
((ExploreFragment) getParentFragment()).setScroll(false);
|
||||
setFragment(mediaDetails, mapFragment);
|
||||
mediaDetails.showImage(position);
|
||||
|
|
@ -192,7 +191,7 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
|||
*/
|
||||
public boolean backPressed() {
|
||||
if (null != mediaDetails && mediaDetails.isVisible()) {
|
||||
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
|
||||
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
removeFragment(mediaDetails);
|
||||
((ExploreFragment) getParentFragment()).setScroll(true);
|
||||
setFragment(mapFragment, mediaDetails);
|
||||
|
|
@ -213,4 +212,11 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
|
|||
((MainActivity) getActivity()).showTabs();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,17 @@ package fr.free.nrw.commons.explore;
|
|||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.SearchView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding2.widget.RxSearchView;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.databinding.ActivitySearchBinding;
|
||||
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
|
||||
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
|
||||
|
|
@ -45,13 +39,6 @@ import timber.log.Timber;
|
|||
public class SearchActivity extends BaseActivity
|
||||
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
@BindView(R.id.toolbar_search) Toolbar toolbar;
|
||||
@BindView(R.id.searchHistoryContainer) FrameLayout searchHistoryContainer;
|
||||
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
|
||||
@BindView(R.id.searchBox) SearchView searchView;
|
||||
@BindView(R.id.tab_layout) TabLayout tabLayout;
|
||||
@BindView(R.id.viewPager) ViewPager viewPager;
|
||||
|
||||
@Inject
|
||||
RecentSearchesDao recentSearchesDao;
|
||||
|
||||
|
|
@ -63,25 +50,28 @@ public class SearchActivity extends BaseActivity
|
|||
private MediaDetailPagerFragment mediaDetails;
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
|
||||
private ActivitySearchBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_search);
|
||||
ButterKnife.bind(this);
|
||||
binding = ActivitySearchBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setTitle(getString(R.string.title_activity_search));
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(binding.toolbarSearch);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v->onBackPressed());
|
||||
binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed());
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
setSearchHistoryFragment();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
setTabs();
|
||||
searchView.setQueryHint(getString(R.string.search_commons));
|
||||
searchView.onActionViewExpanded();
|
||||
searchView.clearFocus();
|
||||
binding.searchBox.setQueryHint(getString(R.string.search_commons));
|
||||
binding.searchBox.onActionViewExpanded();
|
||||
binding.searchBox.clearFocus();
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -113,8 +103,8 @@ public class SearchActivity extends BaseActivity
|
|||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
compositeDisposable.add(RxSearchView.queryTextChanges(searchView)
|
||||
.takeUntil(RxView.detaches(searchView))
|
||||
compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox)
|
||||
.takeUntil(RxView.detaches(binding.searchBox))
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::handleSearch, Timber::e
|
||||
|
|
@ -124,9 +114,9 @@ public class SearchActivity extends BaseActivity
|
|||
private void handleSearch(final CharSequence query) {
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
saveRecentSearch(query.toString());
|
||||
viewPager.setVisibility(View.VISIBLE);
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
searchHistoryContainer.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.VISIBLE);
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.searchHistoryContainer.setVisibility(View.GONE);
|
||||
|
||||
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
||||
searchDepictionsFragment.onQueryUpdated(query.toString());
|
||||
|
|
@ -144,10 +134,10 @@ public class SearchActivity extends BaseActivity
|
|||
else {
|
||||
//Open RecentSearchesFragment
|
||||
recentSearchesFragment.updateRecentSearches();
|
||||
viewPager.setVisibility(View.GONE);
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.GONE);
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
setSearchHistoryFragment();
|
||||
searchHistoryContainer.setVisibility(View.VISIBLE);
|
||||
binding.searchHistoryContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,13 +205,13 @@ public class SearchActivity extends BaseActivity
|
|||
@Override
|
||||
public void onMediaClicked(int index) {
|
||||
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
viewPager.setVisibility(View.GONE);
|
||||
mediaContainer.setVisibility(View.VISIBLE);
|
||||
searchView.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.GONE);
|
||||
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||
binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
|
||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
|
||||
|
|
@ -269,12 +259,12 @@ public class SearchActivity extends BaseActivity
|
|||
}
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
// back to search so show search toolbar and hide navigation toolbar
|
||||
searchView.setVisibility(View.VISIBLE);//set the searchview
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
viewPager.setVisibility(View.VISIBLE);
|
||||
mediaContainer.setVisibility(View.GONE);
|
||||
binding.searchBox.setVisibility(View.VISIBLE);//set the searchview
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.viewPager.setVisibility(View.VISIBLE);
|
||||
binding.mediaContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
toolbar.setVisibility(View.GONE);
|
||||
binding.toolbarSearch.setVisibility(View.GONE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
|
@ -284,15 +274,16 @@ public class SearchActivity extends BaseActivity
|
|||
* @param query Recent Search Query
|
||||
*/
|
||||
public void updateText(String query) {
|
||||
searchView.setQuery(query, true);
|
||||
binding.searchBox.setQuery(query, true);
|
||||
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
|
||||
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
|
||||
viewPager.requestFocus();
|
||||
binding.viewPager.requestFocus();
|
||||
}
|
||||
|
||||
@Override protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//Dispose the disposables when the activity is destroyed
|
||||
compositeDisposable.dispose();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsInterface
|
|||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import fr.free.nrw.commons.upload.structure.depictions.get
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties
|
||||
import fr.free.nrw.commons.wikidata.model.DataValue
|
||||
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
||||
import fr.free.nrw.commons.wikidata.model.Entities
|
||||
import fr.free.nrw.commons.wikidata.model.Statement_partial
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.wikidata.DataValue
|
||||
import org.wikipedia.wikidata.Entities
|
||||
import org.wikipedia.wikidata.Statement_partial
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -23,6 +21,7 @@ import fr.free.nrw.commons.Utils;
|
|||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding;
|
||||
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
|
||||
|
|
@ -57,14 +56,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
|||
@Inject
|
||||
DepictModel depictModel;
|
||||
private String wikidataItemName;
|
||||
@BindView(R.id.mediaContainer)
|
||||
FrameLayout mediaContainer;
|
||||
@BindView(R.id.tab_layout)
|
||||
TabLayout tabLayout;
|
||||
@BindView(R.id.viewPager)
|
||||
ViewPager viewPager;
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
private ActivityWikidataItemDetailsBinding binding;
|
||||
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
private DepictedItem wikidataItem;
|
||||
|
|
@ -72,19 +64,20 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_wikidata_item_details);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(2);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.viewPager.setOffscreenPageLimit(2);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
|
||||
final DepictedItem depictedItem = getIntent().getParcelableExtra(
|
||||
WikidataConstants.BOOKMARKS_ITEMS);
|
||||
wikidataItem = depictedItem;
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setTabs();
|
||||
setPageTitle();
|
||||
|
|
@ -137,7 +130,7 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
|||
fragmentList.add(parentDepictionsFragment);
|
||||
titleList.add(getResources().getString(R.string.title_for_parent_classes));
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPager.setOffscreenPageLimit(2);
|
||||
binding.viewPager.setOffscreenPageLimit(2);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
|
|
@ -148,12 +141,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
|||
*/
|
||||
@Override
|
||||
public void onMediaClicked(int position) {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
viewPager.setVisibility(View.GONE);
|
||||
mediaContainer.setVisibility(View.VISIBLE);
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.GONE);
|
||||
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
|
||||
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
|
||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
|
|
@ -183,9 +176,9 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
|
|||
@Override
|
||||
public void onBackPressed() {
|
||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
viewPager.setVisibility(View.VISIBLE);
|
||||
mediaContainer.setVisibility(View.GONE);
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.viewPager.setVisibility(View.VISIBLE);
|
||||
binding.mediaContainer.setVisibility(View.GONE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ public class ExploreMapCalls {
|
|||
/**
|
||||
* Calls method to query Commons for uploads around a location
|
||||
*
|
||||
* @param curLatLng coordinates of search location
|
||||
* @param currentLatLng coordinates of search location
|
||||
* @return list of places obtained
|
||||
*/
|
||||
List<Media> callCommonsQuery(final LatLng curLatLng) {
|
||||
String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude();
|
||||
List<Media> callCommonsQuery(final LatLng currentLatLng) {
|
||||
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
|
||||
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +1,34 @@
|
|||
package fr.free.nrw.commons.explore.map;
|
||||
|
||||
import android.content.Context;
|
||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
||||
import com.mapbox.mapboxsdk.camera.CameraUpdate;
|
||||
import fr.free.nrw.commons.BaseMarker;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import java.util.List;
|
||||
|
||||
public class ExploreMapContract {
|
||||
|
||||
interface View {
|
||||
boolean isNetworkConnectionEstablished();
|
||||
void populatePlaces(LatLng curlatLng,LatLng searchLatLng);
|
||||
void checkPermissionsAndPerformAction();
|
||||
void populatePlaces(LatLng curlatLng);
|
||||
void askForLocationPermission();
|
||||
void recenterMap(LatLng curLatLng);
|
||||
void showLocationOffDialog();
|
||||
void openLocationSettings();
|
||||
void hideBottomDetailsSheet();
|
||||
void displayBottomSheetWithInfo(Marker marker);
|
||||
void addOnCameraMoveListener();
|
||||
LatLng getMapCenter();
|
||||
LatLng getMapFocus();
|
||||
LatLng getLastMapFocus();
|
||||
void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers);
|
||||
void clearAllMarkers();
|
||||
void addSearchThisAreaButtonAction();
|
||||
void setSearchThisAreaButtonVisibility(boolean isVisible);
|
||||
void setProgressBarVisibility(boolean isVisible);
|
||||
boolean isDetailsBottomSheetVisible();
|
||||
boolean isSearchThisAreaButtonVisible();
|
||||
void addCurrentLocationMarker(LatLng curLatLng);
|
||||
void updateMapToTrackPosition(LatLng curLatLng);
|
||||
Context getContext();
|
||||
LatLng getCameraTarget();
|
||||
void centerMapToPlace(Place placeToCenter);
|
||||
LatLng getLastLocation();
|
||||
com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation();
|
||||
boolean isCurrentLocationMarkerVisible();
|
||||
void setProjectorLatLngBounds();
|
||||
void disableFABRecenter();
|
||||
void enableFABRecenter();
|
||||
void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker);
|
||||
void setMapBoundaries(CameraUpdate cameaUpdate);
|
||||
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
|
||||
boolean backButtonClicked();
|
||||
}
|
||||
|
|
@ -51,9 +40,6 @@ public class ExploreMapContract {
|
|||
void detachView();
|
||||
void setActionListeners(JsonKvStore applicationKvStore);
|
||||
boolean backButtonClicked();
|
||||
void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng);
|
||||
void markerUnselected();
|
||||
void markerSelected(Marker marker);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,11 @@ import com.bumptech.glide.Glide;
|
|||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
||||
import fr.free.nrw.commons.BaseMarker;
|
||||
import fr.free.nrw.commons.MapController;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.LocationUtils;
|
||||
|
|
@ -33,6 +31,7 @@ import javax.inject.Inject;
|
|||
import timber.log.Timber;
|
||||
|
||||
public class ExploreMapController extends MapController {
|
||||
|
||||
private final ExploreMapCalls exploreMapCalls;
|
||||
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
||||
public LatLng currentLocation; // current location of user
|
||||
|
|
@ -46,13 +45,18 @@ public class ExploreMapController extends MapController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Takes location as parameter and returns ExplorePlaces info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
|
||||
* @param curLatLng is current geolocation
|
||||
* @param searchLatLng is the location that we want to search around
|
||||
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around current location, false if another location
|
||||
* @return explorePlacesInfo info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
|
||||
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
|
||||
* explorePlaceList and boundaryCoordinates
|
||||
*
|
||||
* @param currentLatLng is current geolocation
|
||||
* @param searchLatLng is the location that we want to search around
|
||||
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
|
||||
* current location, false if another location
|
||||
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
|
||||
* boundaryCoordinates
|
||||
*/
|
||||
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrentLocation) {
|
||||
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng,
|
||||
boolean checkingAroundCurrentLocation) {
|
||||
|
||||
if (searchLatLng == null) {
|
||||
Timber.d("Loading attractions explore map, but search is null");
|
||||
|
|
@ -61,7 +65,7 @@ public class ExploreMapController extends MapController {
|
|||
|
||||
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
|
||||
try {
|
||||
explorePlacesInfo.curLatLng = curLatLng;
|
||||
explorePlacesInfo.currentLatLng = currentLatLng;
|
||||
latestSearchLocation = searchLatLng;
|
||||
|
||||
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
|
||||
|
|
@ -74,18 +78,23 @@ public class ExploreMapController extends MapController {
|
|||
Timber.d("Sorting places by distance...");
|
||||
final Map<Media, Double> distances = new HashMap<>();
|
||||
for (Media media : mediaList) {
|
||||
distances.put(media, computeDistanceBetween(media.getCoordinates(), searchLatLng));
|
||||
distances.put(media,
|
||||
computeDistanceBetween(media.getCoordinates(), searchLatLng));
|
||||
// Find boundaries with basic find max approach
|
||||
if (media.getCoordinates().getLatitude() < boundaryCoordinates[0].getLatitude()) {
|
||||
if (media.getCoordinates().getLatitude()
|
||||
< boundaryCoordinates[0].getLatitude()) {
|
||||
boundaryCoordinates[0] = media.getCoordinates();
|
||||
}
|
||||
if (media.getCoordinates().getLatitude() > boundaryCoordinates[1].getLatitude()) {
|
||||
if (media.getCoordinates().getLatitude()
|
||||
> boundaryCoordinates[1].getLatitude()) {
|
||||
boundaryCoordinates[1] = media.getCoordinates();
|
||||
}
|
||||
if (media.getCoordinates().getLongitude() < boundaryCoordinates[2].getLongitude()) {
|
||||
if (media.getCoordinates().getLongitude()
|
||||
< boundaryCoordinates[2].getLongitude()) {
|
||||
boundaryCoordinates[2] = media.getCoordinates();
|
||||
}
|
||||
if (media.getCoordinates().getLongitude() > boundaryCoordinates[3].getLongitude()) {
|
||||
if (media.getCoordinates().getLongitude()
|
||||
> boundaryCoordinates[3].getLongitude()) {
|
||||
boundaryCoordinates[3] = media.getCoordinates();
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +105,8 @@ public class ExploreMapController extends MapController {
|
|||
|
||||
// Sets latestSearchRadius to maximum distance among boundaries and search location
|
||||
for (LatLng bound : boundaryCoordinates) {
|
||||
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(bound).distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(latestSearchLocation));
|
||||
double distance = LocationUtils.calculateDistance(bound.getLatitude(),
|
||||
bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude());
|
||||
if (distance > latestSearchRadius) {
|
||||
latestSearchRadius = distance;
|
||||
}
|
||||
|
|
@ -105,7 +115,7 @@ public class ExploreMapController extends MapController {
|
|||
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
|
||||
if (checkingAroundCurrentLocation) {
|
||||
currentLocationSearchRadius = latestSearchRadius;
|
||||
currentLocation = curLatLng;
|
||||
currentLocation = currentLatLng;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
|
@ -115,42 +125,42 @@ public class ExploreMapController extends MapController {
|
|||
|
||||
/**
|
||||
* Loads attractions from location for map view, we need to return places in Place data type
|
||||
*
|
||||
* @return baseMarkerOptions list that holds nearby places with their icons
|
||||
*/
|
||||
public static List<NearbyBaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
|
||||
LatLng curLatLng,
|
||||
public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
|
||||
LatLng currentLatLng,
|
||||
final List<Place> placeList,
|
||||
Context context,
|
||||
NearbyBaseMarkerThumbCallback callback,
|
||||
Marker selectedMarker,
|
||||
boolean shouldTrackPosition,
|
||||
ExplorePlacesInfo explorePlacesInfo) {
|
||||
List<NearbyBaseMarker> baseMarkerOptions = new ArrayList<>();
|
||||
List<BaseMarker> baseMarkerList = new ArrayList<>();
|
||||
|
||||
if (placeList == null) {
|
||||
return baseMarkerOptions;
|
||||
return baseMarkerList;
|
||||
}
|
||||
|
||||
VectorDrawableCompat vectorDrawable = null;
|
||||
try {
|
||||
vectorDrawable = VectorDrawableCompat.create(
|
||||
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme());
|
||||
context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme());
|
||||
|
||||
} catch (Resources.NotFoundException e) {
|
||||
// ignore when running tests.
|
||||
}
|
||||
if (vectorDrawable != null) {
|
||||
for (Place explorePlace : placeList) {
|
||||
final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
|
||||
String distance = formatDistanceBetween(curLatLng, explorePlace.location);
|
||||
final BaseMarker baseMarker = new BaseMarker();
|
||||
String distance = formatDistanceBetween(currentLatLng, explorePlace.location);
|
||||
explorePlace.setDistance(distance);
|
||||
|
||||
nearbyBaseMarker.title(explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
|
||||
nearbyBaseMarker.position(
|
||||
new com.mapbox.mapboxsdk.geometry.LatLng(
|
||||
baseMarker.setTitle(
|
||||
explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
|
||||
baseMarker.setPosition(
|
||||
new fr.free.nrw.commons.location.LatLng(
|
||||
explorePlace.location.getLatitude(),
|
||||
explorePlace.location.getLongitude()));
|
||||
nearbyBaseMarker.place(explorePlace);
|
||||
explorePlace.location.getLongitude(), 0));
|
||||
baseMarker.setPlace(explorePlace);
|
||||
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
|
|
@ -160,12 +170,15 @@ public class ExploreMapController extends MapController {
|
|||
.into(new CustomTarget<Bitmap>() {
|
||||
// We add icons to markers when bitmaps are ready
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromBitmap(
|
||||
ImageUtils.addRedBorder(resource, 6, context)));
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
|
||||
public void onResourceReady(@NonNull Bitmap resource,
|
||||
@Nullable Transition<? super Bitmap> transition) {
|
||||
baseMarker.setIcon(
|
||||
ImageUtils.addRedBorder(resource, 6, context));
|
||||
baseMarkerList.add(baseMarker);
|
||||
if (baseMarkerList.size()
|
||||
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
||||
explorePlacesInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,20 +190,24 @@ public class ExploreMapController extends MapController {
|
|||
@Override
|
||||
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
|
||||
super.onLoadFailed(errorDrawable);
|
||||
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromResource(R.drawable.image_placeholder_96));
|
||||
baseMarkerOptions.add(nearbyBaseMarker);
|
||||
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
|
||||
baseMarker.fromResource(context, R.drawable.image_placeholder_96);
|
||||
baseMarkerList.add(baseMarker);
|
||||
if (baseMarkerList.size()
|
||||
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
||||
explorePlacesInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return baseMarkerOptions;
|
||||
return baseMarkerList;
|
||||
}
|
||||
|
||||
interface NearbyBaseMarkerThumbCallback {
|
||||
|
||||
// Callback to notify thumbnails of explore markers are added as icons and ready
|
||||
void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition);
|
||||
void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
|
||||
ExplorePlacesInfo explorePlacesInfo);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue