mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge branch 'main' into logtests
This commit is contained in:
commit
83e824439c
1482 changed files with 79410 additions and 71768 deletions
85
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
85
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help us improve.
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Before creating an issue, please search the existing issues to see if a similar one has already been created.
|
||||
- You can search issues by specific labels (e.g. `label:nearby`) or just by typing keywords into the search filter.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Summarize your issue (what goes wrong, what did you expect to happen)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How can we reproduce the issue?
|
||||
placeholder: |
|
||||
1. Have the app open..
|
||||
2. Go to..
|
||||
3. Click on..
|
||||
4. Observe..
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
placeholder: A menu should open..
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual behaviour
|
||||
placeholder: The app closes unexpectedly..
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Device information
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device name
|
||||
description: What make and model device did you encounter this on?
|
||||
placeholder: Samsung J7
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Android version
|
||||
description: What Android version (e.g., Android 6.0 Marshmallow or Android 11) are you running? Is it the stock version from the manufacturer or a custom ROM ?
|
||||
placeholder: Android 10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Commons app version
|
||||
description: You can find this information by clicking the right-most menu in the bottom navigation bar in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. `master` and `prodDebug`).
|
||||
placeholder: 3.1.1
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Device logs
|
||||
description: Add logcat files here (if possible). Need help? See "[Getting app logs from Android Studio](https://commons-app.github.io/docs.html#getting-app-logs-from-android-studio)".
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screen-shots
|
||||
description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Would you like to work on the issue?
|
||||
description: Please let us know whether you want to fix the issue by yourself. If not, anyone can get the issue assigned to them.
|
||||
options:
|
||||
- "Yes"
|
||||
- Prefer not
|
||||
validations:
|
||||
required: false
|
||||
30
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: "⭐️ Feature request"
|
||||
description: Suggest an idea for this project
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean.
|
||||
- Every issue should have exactly one feature request described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points.
|
||||
- Feature Requests are better when they’re open-ended instead of demanding a specific solution e.g: “I want an easier way to do X” instead of “add Y”.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What is the user problem or growth opportunity you want to see solved?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: How do you know that this problem exists today? Why is this important?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Who will benefit from it?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else you would like to add?
|
||||
validations:
|
||||
required: false
|
||||
46
.github/ISSUE_TEMPLATE/feedback.yml
vendored
Normal file
46
.github/ISSUE_TEMPLATE/feedback.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: "\U0001F4AC Feedback"
|
||||
description: Share your feedback about the app
|
||||
labels: ["feedback"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Before creating an issue, please search the existing issues to see if a similar one has already been created.
|
||||
- You can search issues by specific labels (e.g. `label:nearby`) or just by typing keywords into the search filter.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feedback
|
||||
description: Share your feedback about the app.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Wiki username
|
||||
placeholder: Jimbo Wales
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Device information
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device name
|
||||
description: What make and model device did you encounter this on?
|
||||
placeholder: Samsung J7
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Android version
|
||||
description: What Android version (e.g., Android 6.0 Marshmallow or Android 11) are you running? Is it the stock version from the manufacturer or a custom ROM ?
|
||||
placeholder: Android 10
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Commons app version
|
||||
description: You can find this information by clicking the right-most menu in the bottom navigation bar in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. `master` and `prodDebug`).
|
||||
placeholder: 3.1.1
|
||||
validations:
|
||||
required: true
|
||||
13
.github/ISSUE_TEMPLATE/need-help.yml
vendored
Normal file
13
.github/ISSUE_TEMPLATE/need-help.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: "✋🏻 Need help"
|
||||
description: Describe the situation which you need help with.
|
||||
labels: ["help needed"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Describe the situation which you need help with with as much information as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
70
.github/workflows/android.yml
vendored
70
.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 }}
|
||||
|
|
@ -8,21 +8,21 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: Build APK and Run Unit Tests
|
||||
name: Run tests and generate APK
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: Checkout code
|
||||
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: 8
|
||||
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
|
||||
|
|
@ -30,20 +30,66 @@ jobs:
|
|||
key: gradle-packages-${{ runner.os }}-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
|
||||
restore-keys: gradle-packages-${{ runner.os }}
|
||||
|
||||
- name: Build with Gradle and run Unit Tests
|
||||
- name: Access test login credentials
|
||||
run: |
|
||||
echo "TEST_USER_NAME=${{ secrets.TEST_USER_NAME }}" >> local.properties
|
||||
echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> local.properties
|
||||
|
||||
- name: AVD cache
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/cache@v3
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-tablet-api-24
|
||||
|
||||
- name: Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 24
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Run Instrumentation tests
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 24
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
profile: Nexus 10
|
||||
script: |
|
||||
adb shell content insert --uri content://settings/system --bind name:s:accelerometer_rotation --bind value:i:0
|
||||
adb shell content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:0
|
||||
adb emu geo fix 37.422131 -122.084801
|
||||
./gradlew connectedBetaDebugAndroidTest --stacktrace
|
||||
|
||||
- name: Run Unit tests with unified coverage
|
||||
if: github.event_name != 'pull_request'
|
||||
run: ./gradlew -Pcoverage testBetaDebugUnitTestUnifiedCoverage --stacktrace
|
||||
|
||||
- name: Run Unit tests without unified coverage
|
||||
if: github.event_name == 'pull_request'
|
||||
run: ./gradlew -Pcoverage testBetaDebugUnitTestCoverage --stacktrace
|
||||
|
||||
- name: Upload Test Report to Codecov
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
chmod +x codecov
|
||||
./codecov -f "app/build/reports/jacoco/testBetaDebugUnitTestCoverage/testBetaDebugUnitTestCoverage.xml" -Z
|
||||
./codecov -f "app/build/reports/jacoco/testBetaDebugUnitTestUnifiedCoverage/testBetaDebugUnitTestUnifiedCoverage.xml" -Z
|
||||
|
||||
- name: Generate betaDebug APK
|
||||
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
|
||||
|
|
@ -52,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>
|
||||
|
|
|
|||
7
.idea/inspectionProfiles/Project_Default.xml
generated
7
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -1,16 +1,12 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="reportWhenNoStatementFollow" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="REPORT_VARIABLES" value="true" />
|
||||
<option name="REPORT_PARAMETERS" value="true" />
|
||||
|
|
@ -25,13 +21,11 @@
|
|||
<option name="ignoreInMatchingInstanceof" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSerializable" value="false" />
|
||||
<option name="ignoreCloneable" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
|
|
@ -47,6 +41,5 @@
|
|||
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
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>
|
||||
168
CHANGELOG.md
168
CHANGELOG.md
|
|
@ -1,5 +1,173 @@
|
|||
# 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
|
||||
|
||||
## v4.0.2
|
||||
- Fixed bug with wrong dates taken from EXIF
|
||||
- Fixed various crashes
|
||||
|
||||
## v4.0.1
|
||||
- Fixed bug with no browser found
|
||||
- Updated Mapbox SDK to fix hamburger crash
|
||||
|
||||
## v4.0.0
|
||||
- Added map showing nearby Commons pictures
|
||||
- Added custom SPARQL queries
|
||||
- Added user profiles
|
||||
- Added custom picture selector
|
||||
- Various bugfixes
|
||||
- Updated target SDK to 30
|
||||
|
||||
## v3.1.1
|
||||
- Optimized Nearby query
|
||||
- Added Sweden's property for WLM 2021
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
**Summary:**
|
||||
|
||||
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
|
||||
|
||||
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby`) or just by typing keywords into the search filter._
|
||||
|
||||
**Steps to reproduce:**
|
||||
|
||||
How can we reproduce the issue?
|
||||
What did you expect the app to do, and what did you see instead?
|
||||
|
||||
**System logs:**
|
||||
|
||||
```
|
||||
Add logcat files here (if possible).
|
||||
|
||||
Need help? See https://github.com/commons-app/apps-android-commons/wiki/Getting-app-logs-from-Android-Studio
|
||||
```
|
||||
|
||||
**Device and Android version:**
|
||||
|
||||
What make and model device (e.g., Samsung J7) did you encounter this on?
|
||||
What Android version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running?
|
||||
Is it the stock version from the manufacturer or a custom ROM ?
|
||||
|
||||
**Commons app version:**
|
||||
|
||||
You can find this information by going to the navigation drawer in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. master and prodDebug).
|
||||
|
||||
**Screen-shots:**
|
||||
|
||||
Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
||||
|
||||
**Would you like to work on the issue?**
|
||||
|
||||
Please let us know whether you want to fix the issue by yourself. If not, anyone can get the issue assigned to them.
|
||||
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).
|
||||
|
|
|
|||
219
app/build.gradle
219
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'
|
||||
|
|
@ -38,17 +42,31 @@ dependencies {
|
|||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.1.0'
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v8:0.11.0'
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-scalebar-v9:0.4.0'
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-android-telemetry:6.1.0'
|
||||
implementation "org.maplibre.gl:android-sdk:$MAPLIBRE_VERSION"
|
||||
implementation 'org.maplibre.gl:android-plugin-scalebar-v9:1.0.0'
|
||||
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
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"
|
||||
// Jetpack Compose
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
|
||||
|
||||
implementation "androidx.activity:activity-compose:1.9.3"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
||||
implementation (composeBom)
|
||||
implementation "androidx.compose.runtime:runtime"
|
||||
implementation "androidx.compose.ui:ui"
|
||||
implementation "androidx.compose.ui:ui-viewbinding"
|
||||
implementation "androidx.compose.ui:ui-graphics"
|
||||
implementation "androidx.compose.ui:ui-tooling"
|
||||
implementation "androidx.compose.foundation:foundation"
|
||||
implementation "androidx.compose.foundation:foundation-layout"
|
||||
implementation "androidx.compose.material3:material3"
|
||||
androidTestImplementation(composeBom)
|
||||
|
||||
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,52 +91,57 @@ 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"
|
||||
testImplementation("io.mockk:mockk:1.13.5")
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.6-alpha-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 "com.jraska.livedata:testing-ktx:1.2.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.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
|
||||
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'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.1-alpha04'
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.annotation:annotation:1.3.0'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
|
||||
// Debugging
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY_VERSION"
|
||||
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY_VERSION"
|
||||
|
||||
// Support libraries
|
||||
implementation "com.google.android.material:material:1.1.0-alpha04"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||
|
||||
//swipe_layout
|
||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
||||
|
|
@ -129,7 +152,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
|
||||
|
|
@ -137,52 +159,87 @@ 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.6.0"
|
||||
def work_version = "2.8.1"
|
||||
// Kotlin + coroutines
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation("androidx.work:work-runtime:$work_version")
|
||||
testImplementation "androidx.work:work-testing:$work_version"
|
||||
|
||||
//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.2") { exclude group: 'com.google.android', module: 'android' }
|
||||
implementation("io.github.coordinates2country:coordinates2country-android:1.8") { 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) {
|
||||
def adb = "$System.env.ANDROID_HOME/platform-tools/adb"
|
||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'window_animation_scale', '0'
|
||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'transition_animation_scale', '0'
|
||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'animator_duration_scale', '0'
|
||||
}
|
||||
|
||||
project.gradle.taskGraph.whenReady {
|
||||
connectedBetaDebugAndroidTest.dependsOn disableAnimations
|
||||
connectedProdDebugAndroidTest.dependsOn disableAnimations
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
//applicationId 'fr.free.nrw.commons'
|
||||
|
||||
versionCode 1025
|
||||
versionName '3.1.1'
|
||||
versionCode 1040
|
||||
versionName '5.0.2'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 30
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
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', '/META-INF/LICENSE.md', '/META-INF/LICENSE-notice.md']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.includeAndroidResources = true
|
||||
animationsDisabled true
|
||||
|
||||
unitTests {
|
||||
returnDefaultValues = true
|
||||
includeAndroidResources = true
|
||||
}
|
||||
|
||||
unitTests.all {
|
||||
jvmArgs '-noverify'
|
||||
|
|
@ -207,16 +264,18 @@ 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 project.hasProperty('coverage')
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
testProguardFile 'test-proguard-rules.txt'
|
||||
versionNameSuffix "-debug-" + getBranchName()
|
||||
enableUnitTestCoverage true
|
||||
enableAndroidTestCoverage true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +289,8 @@ android {
|
|||
|
||||
configurations.all {
|
||||
resolutionStrategy.force 'androidx.annotation:annotation:1.1.0'
|
||||
resolutionStrategy.force 'com.jakewharton.timber:timber:4.7.1'
|
||||
resolutionStrategy.force 'androidx.fragment:fragment:1.3.6'
|
||||
exclude module: 'okhttp-ws'
|
||||
}
|
||||
flavorDimensions 'tier'
|
||||
|
|
@ -253,19 +314,20 @@ android {
|
|||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
||||
buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\""
|
||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\""
|
||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
|
||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
|
||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
|
||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
|
||||
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
||||
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
|
||||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
||||
|
||||
dimension 'tier'
|
||||
}
|
||||
|
||||
|
|
@ -288,40 +350,61 @@ android {
|
|||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
||||
buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\""
|
||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\""
|
||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
|
||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
|
||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\""
|
||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\""
|
||||
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
||||
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
|
||||
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_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildToolsVersion buildToolsVersion
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.5.8'
|
||||
}
|
||||
namespace 'fr.free.nrw.commons'
|
||||
lint {
|
||||
abortOnError false
|
||||
disable 'MissingTranslation', 'ExtraTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
String getTestUserName() {
|
||||
def propFile = rootProject.file("./local.properties")
|
||||
def properties = new Properties()
|
||||
properties.load(new FileInputStream(propFile))
|
||||
return properties['TEST_USER_NAME']
|
||||
}
|
||||
|
||||
String getTestPassword() {
|
||||
def propFile = rootProject.file("./local.properties")
|
||||
def properties = new Properties()
|
||||
properties.load(new FileInputStream(propFile))
|
||||
return properties['TEST_USER_PASSWORD']
|
||||
}
|
||||
|
||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||
|
|
@ -337,7 +420,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 ---
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons
|
|||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
|
|
@ -12,10 +11,13 @@ import androidx.test.espresso.intent.Intents
|
|||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -26,84 +28,122 @@ class AboutActivityTest {
|
|||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
Intents.init()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuildNumber() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_version))
|
||||
.check(ViewAssertions.matches(
|
||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
|
||||
))
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.about_version))
|
||||
.check(
|
||||
ViewAssertions.matches(
|
||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchWebsite() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.website_launch_icon)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.WEBSITE_URL)))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.WEBSITE_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchFacebook() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.facebook_launch_icon)).perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasAction(Intent.ACTION_VIEW))
|
||||
Intents.intended(CoreMatchers.anyOf(IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
||||
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME)))
|
||||
Intents.intended(
|
||||
CoreMatchers.anyOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
||||
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchGithub() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.github_launch_icon)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.GITHUB_REPO_URL)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchRateUs() {
|
||||
val appPackageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_rate_us)).perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasAction(Intent.ACTION_VIEW))
|
||||
Intents.intended(CoreMatchers.anyOf(IntentMatchers.hasData("${Urls.PLAY_STORE_URL_PREFIX}$appPackageName"),
|
||||
IntentMatchers.hasData("${Urls.PLAY_STORE_URL_PREFIX}$appPackageName")))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.GITHUB_REPO_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAboutPrivacyPolicy() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_privacy_policy)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL)))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchTranslate() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click())
|
||||
val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0]
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode")))
|
||||
val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0]
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAboutCredits() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_credits)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.CREDITS_URL)))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.CREDITS_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchUserGuide() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAboutFaq() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.FAQ_URL)))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.FAQ_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.contrib.DrawerActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AchievementsActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
UITestHelper.skipWelcome()
|
||||
UITestHelper.loginUser()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAchievements() {
|
||||
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
|
||||
|
||||
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.runner.RunWith
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.core.AllOf
|
||||
import org.junit.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DepictionSearchTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(UploadActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun TestForCaptionsAndDepictions() {
|
||||
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
|
||||
|
||||
// Build a result to return from the Camera app
|
||||
|
||||
|
||||
// Stub out the File picker. When an intent is sent to the File picker, this tells
|
||||
// Espresso to respond with the ActivityResult we just created
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
|
||||
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
|
||||
.perform(ViewActions.click())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.PerformException
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.contrib.DrawerActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LeaderboardActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
UITestHelper.skipWelcome()
|
||||
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testScrollToRankFromAbove() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(selectTabAtPosition(1))
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.scroll)).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testScrollToRankFromBelow() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(selectTabAtPosition(1))
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.leaderboard_list)).perform(ViewActions.swipeUp())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.leaderboard_list)).perform(ViewActions.swipeUp())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.scroll)).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
private fun selectTabAtPosition(tabIndex: Int): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getDescription() = "with tab at index $tabIndex"
|
||||
|
||||
override fun getConstraints() = allOf(isDisplayed(), isAssignableFrom(TabLayout::class.java))
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
val tabLayout = view as TabLayout
|
||||
val tabAtIndex: TabLayout.Tab = tabLayout.getTabAt(tabIndex)
|
||||
?: throw PerformException.Builder()
|
||||
.withCause(Throwable("No tab at index $tabIndex"))
|
||||
.build()
|
||||
|
||||
tabAtIndex.select()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,15 +8,17 @@ import androidx.test.espresso.action.ViewActions
|
|||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.auth.SignupActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -27,33 +29,41 @@ class LoginActivityTest {
|
|||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
Intents.init()
|
||||
UITestHelper.skipWelcome()
|
||||
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLogin() {
|
||||
UITestHelper.loginUser()
|
||||
Intents.intended(hasComponent(MainActivity::class.java.name))
|
||||
@After
|
||||
fun cleanUp() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForgotPassword() {
|
||||
UITestHelper.sleep(3000)
|
||||
Espresso.onView(ViewMatchers.withId(R.id.forgot_password))
|
||||
.perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL)));
|
||||
Espresso.onView(ViewMatchers.withId(R.id.forgot_password)).perform(ViewActions.click())
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSignupButton() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.sign_up_button)).perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasComponent(SignupActivity::class.java.name))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,214 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.google.gson.Gson
|
||||
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.notification.NotificationActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(MainActivity::class.java)
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@get:Rule
|
||||
var mGrantPermissionRule: GrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
)
|
||||
|
||||
private val device: UiDevice =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
private lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents.init()
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val storeName = context.packageName + "_preferences"
|
||||
defaultKvStore = JsonKvStore(context, storeName, Gson())
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
fun testNearby() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
UITestHelper.sleep(10000)
|
||||
val actionMenuItemView2 =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.list_sheet),
|
||||
ViewMatchers.withContentDescription("List"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
)
|
||||
actionMenuItemView2.perform(ViewActions.click())
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExplore() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContributions() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.contributionImage),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.contributionsList),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
val actionMenuItemView =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
)
|
||||
actionMenuItemView.perform(ViewActions.click())
|
||||
UITestHelper.sleep(3000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBookmarks() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
3,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotifications() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.notifications),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
1,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
|
||||
Espresso.pressBack()
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.intent.rule.IntentsTestRule
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ProfileActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProfile() {
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withContentDescription("More"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
4,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
||||
ViewActions.scrollTo(),
|
||||
ViewActions.click(),
|
||||
)
|
||||
device.swipe(1033, 1346, 531, 1346, 20)
|
||||
UITestHelper.sleep(5000)
|
||||
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import fr.free.nrw.commons.review.ReviewActivity
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReviewActivityTest {
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,59 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.explore.SearchActivity
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import fr.free.nrw.commons.explore.SearchActivity
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SearchActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
private val device: UiDevice =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exploreActivityTest() {
|
||||
val searchAutoComplete =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
UITestHelper.childAtPosition(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||
UITestHelper.childAtPosition(
|
||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||
1,
|
||||
),
|
||||
),
|
||||
0,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
)
|
||||
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
|
||||
UITestHelper.sleep(5000)
|
||||
device.swipe(1000, 1400, 500, 1400, 20)
|
||||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.settings.SettingsActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SettingsActivityLoggedInTest {
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
private val device: UiDevice =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSettings() {
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withContentDescription("More"),
|
||||
UITestHelper.childAtPosition(
|
||||
UITestHelper.childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
4,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
|
||||
ViewActions.scrollTo(),
|
||||
ViewActions.click(),
|
||||
)
|
||||
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,26 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.PreferenceMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.google.gson.Gson
|
||||
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
import fr.free.nrw.commons.settings.SettingsActivity
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.core.IsNot.not
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SettingsActivityTest {
|
||||
private lateinit var defaultKvStore: JsonKvStore
|
||||
|
|
@ -30,125 +28,39 @@ class SettingsActivityTest {
|
|||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(SettingsActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val storeName = context.packageName + "_preferences"
|
||||
defaultKvStore = JsonKvStore(context, storeName, Gson())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setRecentUploadLimitTo123() {
|
||||
// Open "Use external storage" preference
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.perform(click())
|
||||
|
||||
// Try setting it to 123
|
||||
Espresso.onView(withId(android.R.id.edit))
|
||||
.perform(replaceText("123"))
|
||||
|
||||
// Click "OK"
|
||||
Espresso.onView(allOf(withId(android.R.id.button1), withText("OK")))
|
||||
.perform(click())
|
||||
|
||||
// Check setting set to 123 in SharedPreferences
|
||||
assertEquals(
|
||||
123,
|
||||
defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 0).toLong()
|
||||
)
|
||||
|
||||
// Check displaying 123 in summary text
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.onChildView(withId(android.R.id.summary))
|
||||
.check(matches(withText("123")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setRecentUploadLimitTo0() {
|
||||
// Open "Use external storage" preference
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.perform(click())
|
||||
|
||||
// Try setting it to 0
|
||||
Espresso.onView(withId(android.R.id.edit))
|
||||
.perform(replaceText("0"))
|
||||
|
||||
// Click "OK"
|
||||
Espresso.onView(allOf(withId(android.R.id.button1), withText("OK")))
|
||||
.perform(click())
|
||||
|
||||
// Check setting set to 100 in SharedPreferences
|
||||
assertEquals(
|
||||
100,
|
||||
defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 0).toLong()
|
||||
)
|
||||
|
||||
// Check displaying 100 in summary text
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.onChildView(withId(android.R.id.summary))
|
||||
.check(matches(withText("100")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setRecentUploadLimitTo700() {
|
||||
// Open "Use external storage" preference
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.perform(click())
|
||||
|
||||
// Try setting it to 700
|
||||
Espresso.onView(withId(android.R.id.edit))
|
||||
.perform(replaceText("700"))
|
||||
|
||||
// Click "OK"
|
||||
Espresso.onView(allOf(withId(android.R.id.button1), withText("OK")))
|
||||
.perform(click())
|
||||
|
||||
// Check setting set to 500 in SharedPreferences
|
||||
assertEquals(
|
||||
500,
|
||||
defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 0).toLong()
|
||||
)
|
||||
|
||||
// Check displaying 100 in summary text
|
||||
Espresso.onData(PreferenceMatchers.withKey("uploads"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.onChildView(withId(android.R.id.summary))
|
||||
.check(matches(withText("500")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAuthorNameTogglesOn() {
|
||||
// Turn on "Use author name" preference if currently off
|
||||
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||
Espresso.onData(PreferenceMatchers.withKey("useAuthorName"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.perform(click())
|
||||
Espresso
|
||||
.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0),
|
||||
),
|
||||
).perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()),
|
||||
)
|
||||
}
|
||||
|
||||
// Check authorName preference is enabled
|
||||
Espresso.onData(PreferenceMatchers.withKey("authorName"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.check(matches(isEnabled()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAuthorNameTogglesOff() {
|
||||
// Turn off "Use external storage" preference if currently on
|
||||
if (defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||
Espresso.onData(PreferenceMatchers.withKey("useAuthorName"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
// Check authorName preference is enabled
|
||||
Espresso.onData(PreferenceMatchers.withKey("authorName"))
|
||||
.inAdapterView(withId(android.R.id.list))
|
||||
.check(matches(not(isEnabled())))
|
||||
Espresso
|
||||
.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0),
|
||||
),
|
||||
).check(matches(isEnabled()))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.auth.SignupActivity
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SignupTest {
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
UITestHelper.skipWelcome()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSignupButton() {
|
||||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
Espresso.onView(withId(R.id.sign_up_button))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.perform(click())
|
||||
intended(hasComponent(SignupActivity::class.java.name))
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@ package fr.free.nrw.commons
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.test.espresso.Espresso.closeSoftKeyboard
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
|
|
@ -12,36 +13,141 @@ import org.apache.commons.lang3.StringUtils
|
|||
import org.hamcrest.BaseMatcher
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class UITestHelper {
|
||||
companion object {
|
||||
fun skipWelcome() {
|
||||
try {
|
||||
//Skip tutorial
|
||||
onView(ViewMatchers.withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
// Skip tutorial
|
||||
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
.perform(ViewActions.click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun skipLogin() {
|
||||
try {
|
||||
// Skip Login
|
||||
val htmlTextView =
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.skip_login),
|
||||
ViewMatchers.withText("Skip"),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
)
|
||||
htmlTextView.perform(ViewActions.click())
|
||||
|
||||
val appCompatButton =
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(android.R.id.button1),
|
||||
ViewMatchers.withText("Yes"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.buttonPanel),
|
||||
0,
|
||||
),
|
||||
3,
|
||||
),
|
||||
),
|
||||
)
|
||||
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loginUser() {
|
||||
try {
|
||||
//Perform Login
|
||||
// Perform Login
|
||||
sleep(3000)
|
||||
onView(ViewMatchers.withId(R.id.login_username))
|
||||
.perform(ViewActions.clearText(), ViewActions.typeText(getTestUsername()))
|
||||
closeSoftKeyboard()
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUsername()),
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_password))
|
||||
.perform(ViewActions.clearText(), ViewActions.typeText(getTestUserPassword()))
|
||||
closeSoftKeyboard()
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUserPassword()),
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_button))
|
||||
.perform(ViewActions.click())
|
||||
.perform(ViewActions.click())
|
||||
sleep(10000)
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun logoutUser() {
|
||||
try {
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withContentDescription("More"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
4,
|
||||
),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.more_logout),
|
||||
ViewMatchers.withText("Logout"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
||||
0,
|
||||
),
|
||||
6,
|
||||
),
|
||||
),
|
||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(android.R.id.button1),
|
||||
ViewMatchers.withText("Yes"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.buttonPanel),
|
||||
0,
|
||||
),
|
||||
3,
|
||||
),
|
||||
),
|
||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
sleep(5000)
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun childAtPosition(
|
||||
parentMatcher: Matcher<View>,
|
||||
position: Int,
|
||||
): Matcher<View> {
|
||||
return object : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("Child at position $position in parent ")
|
||||
parentMatcher.describeTo(description)
|
||||
}
|
||||
|
||||
public override fun matchesSafely(view: View): Boolean {
|
||||
val parent = view.parent
|
||||
return parent is ViewGroup &&
|
||||
parentMatcher.matches(parent) &&
|
||||
view == parent.getChildAt(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sleep(timeInMillis: Long) {
|
||||
|
|
@ -57,16 +163,21 @@ class UITestHelper {
|
|||
val username = BuildConfig.TEST_USERNAME
|
||||
if (StringUtils.isEmpty(username) || username == "null") {
|
||||
throw NotImplementedError("Configure your beta account's username")
|
||||
} else return username
|
||||
} else {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTestUserPassword(): String {
|
||||
val password = BuildConfig.TEST_PASSWORD
|
||||
if (StringUtils.isEmpty(password) || password == "null") {
|
||||
throw NotImplementedError("Configure your beta account's password")
|
||||
} else return password
|
||||
} else {
|
||||
return password
|
||||
}
|
||||
}
|
||||
fun <T: Activity> changeOrientation(activityRule: ActivityTestRule<T>){
|
||||
|
||||
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
|
|
@ -76,6 +187,7 @@ class UITestHelper {
|
|||
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
||||
return object : BaseMatcher<T>() {
|
||||
var isFirst = true
|
||||
|
||||
override fun matches(item: Any): Boolean {
|
||||
if (isFirst && matcher.matches(item)) {
|
||||
isFirst = false
|
||||
|
|
@ -90,4 +202,4 @@ class UITestHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.core.AllOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -25,25 +16,4 @@ class UploadActivityTest {
|
|||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun TestForCaptionsAndDepictions() {
|
||||
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
|
||||
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
|
||||
.perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasComponent(DepictsFragment::class.java.name))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.locationpicker.LocationPickerActivity
|
||||
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UploadCancelledTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
var mGrantPermissionRule: GrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
)
|
||||
|
||||
private val device: UiDevice =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
}
|
||||
device.unfreezeRotation()
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
try {
|
||||
Intents.release()
|
||||
} catch (ex: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uploadCancelledAfterLocationPickedTest() {
|
||||
val bottomNavigationItemView =
|
||||
onView(
|
||||
allOf(
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
bottomNavigationItemView.perform(click())
|
||||
|
||||
UITestHelper.sleep(12000)
|
||||
|
||||
val actionMenuItemView =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.list_sheet),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
actionMenuItemView.perform(click())
|
||||
|
||||
val recyclerView =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.rv_nearby_list),
|
||||
),
|
||||
)
|
||||
recyclerView.perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||
0,
|
||||
click(),
|
||||
),
|
||||
)
|
||||
|
||||
val linearLayout3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.cameraButton),
|
||||
childAtPosition(
|
||||
allOf(
|
||||
withId(R.id.nearby_button_layout),
|
||||
),
|
||||
0,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
linearLayout3.perform(click())
|
||||
|
||||
val pasteSensitiveTextInputEditText =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.caption_item_edit_text),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.caption_item_edit_text_input_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
||||
|
||||
val pasteSensitiveTextInputEditText2 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.description_item_edit_text),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.description_item_edit_text_input_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
||||
|
||||
val appCompatButton2 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.btn_next),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.ll_container_media_detail),
|
||||
2,
|
||||
),
|
||||
1,
|
||||
),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
appCompatButton2.perform(click())
|
||||
|
||||
val appCompatButton3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(android.R.id.button1),
|
||||
),
|
||||
)
|
||||
appCompatButton3.perform(scrollTo(), click())
|
||||
|
||||
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
||||
|
||||
val floatingActionButton3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.location_chosen_button),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
UITestHelper.sleep(2000)
|
||||
floatingActionButton3.perform(click())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import android.graphics.Bitmap
|
|||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
|
|
@ -20,11 +19,14 @@ import androidx.test.espresso.intent.Intents.intended
|
|||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withParent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||
import fr.free.nrw.commons.util.MyViewAction
|
||||
|
|
@ -32,6 +34,7 @@ import fr.free.nrw.commons.utils.ConfigUtils
|
|||
import org.hamcrest.core.AllOf.allOf
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -40,14 +43,18 @@ import java.io.File
|
|||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Random
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UploadTest {
|
||||
@get:Rule
|
||||
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION)!!
|
||||
var permissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)!!
|
||||
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
|
@ -65,10 +72,9 @@ class UploadTest {
|
|||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
UITestHelper.skipWelcome()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
@ -77,6 +83,7 @@ class UploadTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Fix Failing Test")
|
||||
fun testUploadWithDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
|
|
@ -97,14 +104,13 @@ class UploadTest {
|
|||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -112,29 +118,30 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
|
@ -142,13 +149,14 @@ class UploadTest {
|
|||
private fun dismissWarning(warningText: String) {
|
||||
try {
|
||||
onView(withText(warningText))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Fix Failing Test")
|
||||
fun testUploadWithoutDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
|
|
@ -169,10 +177,10 @@ class UploadTest {
|
|||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -180,34 +188,36 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Fix Failing Test")
|
||||
fun testUploadWithMultilingualDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
|
|
@ -228,28 +238,29 @@ class UploadTest {
|
|||
dismissWarningDialog()
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||
0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(withId(R.id.btn_add_description))
|
||||
.perform(click())
|
||||
onView(withId(R.id.btn_add))
|
||||
.perform(click())
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
||||
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||
1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -257,29 +268,30 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
|
@ -312,7 +324,6 @@ class UploadTest {
|
|||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,8 +345,8 @@ class UploadTest {
|
|||
private fun dismissWarningDialog() {
|
||||
try {
|
||||
onView(withText("Yes"))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
|
@ -343,10 +354,10 @@ class UploadTest {
|
|||
private fun openGallery() {
|
||||
// Open FAB
|
||||
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
// Click gallery
|
||||
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,20 @@ import androidx.test.espresso.action.ViewActions
|
|||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import fr.free.nrw.commons.utils.ConfigUtils
|
||||
import org.hamcrest.core.IsNot.not
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -21,86 +26,108 @@ class WelcomeActivityTest {
|
|||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifBetaShowsSkipButton() {
|
||||
if (ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifProdHidesSkipButton() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.check(matches(not(isDisplayed())))
|
||||
.check(matches(not(isDisplayed())))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBetaSkipButton() {
|
||||
if (ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
assert(activityRule.activity.isDestroyed)
|
||||
.perform(ViewActions.click())
|
||||
assertThat(activityRule.activity.isDestroyed, equalTo(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSwipingOnce() {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assert(true)
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assertThat(true, equalTo(true))
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeRight())
|
||||
assert(true)
|
||||
.perform(ViewActions.swipeRight())
|
||||
assertThat(true, equalTo(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSwipingWholeTutorial() {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assert(true)
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assertThat(true, equalTo(true))
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
assert(true)
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
assertThat(true, equalTo(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swipeBeyondBounds(){
|
||||
var view_pager=activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
fun swipeBeyondBounds() {
|
||||
val viewPager = activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
|
||||
view_pager.adapter?.let { view_pager.currentItem == view_pager.adapter?.count?.minus(1)
|
||||
if (view_pager.currentItem==3){
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assert(true)
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeRight())
|
||||
assert(false)
|
||||
}}
|
||||
viewPager.adapter?.let {
|
||||
if (viewPager.currentItem == 3) {
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeLeft())
|
||||
assertThat(true, equalTo(true))
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeRight())
|
||||
assertThat(true, equalTo(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swipeTillLastAndFinish(){
|
||||
var view_pager=activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
fun swipeTillLastAndFinish() {
|
||||
val viewPager = activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
|
||||
view_pager.adapter?.let { view_pager.currentItem == view_pager.adapter?.count?.minus(1)
|
||||
if (view_pager.currentItem==3){
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
assert(activityRule.activity.isDestroyed)
|
||||
}}
|
||||
viewPager.adapter?.let {
|
||||
if (viewPager.currentItem == 3) {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
assertThat(activityRule.activity.isDestroyed, equalTo(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,34 @@
|
|||
package fr.free.nrw.commons.ui
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.Exception
|
||||
import kotlin.Throws
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PasteSensitiveTextInputEditTextTest {
|
||||
|
||||
private var context: Context? = null
|
||||
private var textView: PasteSensitiveTextInputEditText? = null
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
textView = PasteSensitiveTextInputEditText(context)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTextContextMenuItemPasteFormattingDisabled() {
|
||||
textView!!.setFormattingAllowed(false);
|
||||
textView!!.setText("Text")
|
||||
textView!!.onTextContextMenuItem(R.id.paste)
|
||||
Assert.assertEquals("Text", textView!!.text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTextContextMenuItemPasteFormattingAllowed() {
|
||||
textView!!.setFormattingAllowed(true);
|
||||
textView!!.setText("Text")
|
||||
textView!!.onTextContextMenuItem(R.id.paste)
|
||||
Assert.assertEquals("Text", textView!!.text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTextContextMenuItemPaste() {
|
||||
textView!!.setText("Text")
|
||||
textView!!.onTextContextMenuItem(R.id.paste)
|
||||
Assert.assertEquals("Text", textView!!.text.toString())
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun onTextContextMenuItemNotPaste() {
|
||||
textView!!.setText("Text")
|
||||
textView!!.onTextContextMenuItem(R.id.copy)
|
||||
Assert.assertEquals("Text", textView!!.text.toString())
|
||||
textView = PasteSensitiveTextInputEditText(context!!)
|
||||
}
|
||||
|
||||
// this test has no real value, just % for test code coverage
|
||||
@Test
|
||||
fun extractFormattingAttributeSet(){
|
||||
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod(
|
||||
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java)
|
||||
fun extractFormattingAttributeSet() {
|
||||
val methodExtractFormattingAttribute =
|
||||
textView!!.javaClass.getDeclaredMethod(
|
||||
"extractFormattingAttribute",
|
||||
Context::class.java,
|
||||
AttributeSet::class.java,
|
||||
)
|
||||
methodExtractFormattingAttribute.isAccessible = true
|
||||
methodExtractFormattingAttribute.invoke(textView, context, null)
|
||||
}
|
||||
|
|
@ -76,4 +43,4 @@ class PasteSensitiveTextInputEditTextTest {
|
|||
textView!!.setFormattingAllowed(false)
|
||||
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,56 +9,58 @@ import org.hamcrest.Matcher
|
|||
|
||||
class MyViewAction {
|
||||
companion object {
|
||||
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun typeTextInChildViewWithId(
|
||||
id: Int,
|
||||
textToBeTyped: String,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as EditText
|
||||
v.setText(textToBeTyped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun selectSpinnerItemInChildViewWithId(
|
||||
id: Int,
|
||||
position: Int,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as AppCompatSpinner
|
||||
v.setSelection(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clickItemWithId(id: Int, position: Int): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun clickItemWithId(
|
||||
id: Int,
|
||||
position: Int,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as View
|
||||
v.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,232 +1,261 @@
|
|||
<?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" />
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<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"
|
||||
android:maxSdkVersion="29"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<!-- Permission needed up to Android 5.1, see https://github.com/commons-app/apps-android-commons/pull/5863 -->
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"
|
||||
android:maxSdkVersion="22"/>
|
||||
<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"
|
||||
android:minSdkVersion="33"/>
|
||||
<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"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
|
||||
android:minSdkVersion="34"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<queries>
|
||||
|
||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<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">
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<activity
|
||||
android:name=".description.DescriptionEditActivity"
|
||||
android:exported="true" />
|
||||
<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" />
|
||||
|
||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||
android:process=":acra"
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
<application
|
||||
android:name=".CommonsApplication"
|
||||
android:appComponentFactory="commons"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/LightAppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:appComponentFactory">
|
||||
<activity
|
||||
android:name=".nearby.WikidataFeedback"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".upload.UploadProgressActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".description.DescriptionEditActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/EditActivityTheme" />
|
||||
<activity
|
||||
android:name=".edit.EditActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="org.acra.dialog.CrashReportDialog"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:process=":acra" />
|
||||
<activity
|
||||
android:name=".media.ZoomableActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:label="Zoomable Activity"
|
||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||
<activity
|
||||
android:name=".auth.LoginActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
<activity
|
||||
android:name=".media.ZoomableActivity" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
|
||||
<activity android:name=".auth.LoginActivity">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity android:name=".WelcomeActivity" />
|
||||
<activity
|
||||
android:name=".upload.UploadActivity"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
<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>
|
||||
<activity android:name=".WelcomeActivity" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<activity
|
||||
android:hardwareAccelerated="false"
|
||||
android:name=".upload.UploadActivity"
|
||||
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" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".contributions.MainActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
/>
|
||||
<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=".auth.SignupActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/title_activity_signup" />
|
||||
<activity
|
||||
android:name=".notification.NotificationActivity"
|
||||
android:label="@string/navigation_item_notification" />
|
||||
<activity
|
||||
android:name=".quiz.QuizActivity"
|
||||
android:label="@string/quiz" />
|
||||
<activity
|
||||
android:name=".quiz.QuizResultActivity"
|
||||
android:label="@string/result" />
|
||||
<activity
|
||||
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:label="@string/title_activity_custom_selector"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity
|
||||
android:name=".category.CategoryDetailsActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity
|
||||
android:name=".explore.depictions.WikidataItemDetailsActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity
|
||||
android:name=".explore.SearchActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_search"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:label="@string/Profile" />
|
||||
<activity
|
||||
android:name=".review.ReviewActivity"
|
||||
android:label="@string/title_activity_review" />
|
||||
<activity
|
||||
android:name=".locationpicker.LocationPickerActivity"
|
||||
android:label="Location Picker" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<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" />
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
android:exported="false"
|
||||
android:process=":acra" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<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=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
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=".recentlanguages.RecentLanguagesContentProvider"
|
||||
android:authorities="${applicationId}.recentlanguages.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_recent_languages"
|
||||
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=".bookmarks.locations.BookmarkLocationsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
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" />
|
||||
|
||||
<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" />
|
||||
<receiver
|
||||
android:name=".widget.PicOfDayAppWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<activity
|
||||
android:name=".auth.SignupActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/title_activity_signup" />
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".notification.NotificationActivity"
|
||||
android:label="@string/navigation_item_notification" />
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false" />
|
||||
</application>
|
||||
|
||||
<activity android:name=".quiz.QuizActivity"
|
||||
android:label="@string/quiz"/>
|
||||
|
||||
<activity android:name=".quiz.QuizResultActivity"
|
||||
android:label="@string/result"/>
|
||||
|
||||
<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=".category.CategoryDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<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=".explore.SearchActivity"
|
||||
android:label="@string/title_activity_search"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:parentActivityName=".contributions.MainActivity"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:configChanges="orientation|screenSize|keyboard"
|
||||
android:label="@string/Profile" />
|
||||
|
||||
<activity
|
||||
android:name=".review.ReviewActivity"
|
||||
android:label="@string/title_activity_review" />
|
||||
|
||||
<activity
|
||||
android:name=".LocationPicker.LocationPickerActivity"
|
||||
android:label="Location Picker" />
|
||||
|
||||
<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>
|
||||
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
android:exported="false"
|
||||
android:process=":acra" />
|
||||
|
||||
<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=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
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.pictures.BookmarkPicturesContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks"
|
||||
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=".bookmarks.items.BookmarkItemsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
|
||||
<receiver android:name=".widget.PicOfDayAppWidget">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -16,6 +15,7 @@ import androidx.annotation.NonNull;
|
|||
import fr.free.nrw.commons.databinding.ActivityAboutBinding;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -64,6 +64,7 @@ public class AboutActivity extends BaseActivity {
|
|||
|
||||
Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext());
|
||||
Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext());
|
||||
Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext());
|
||||
Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext());
|
||||
Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext());
|
||||
Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext());
|
||||
|
|
@ -77,6 +78,7 @@ public class AboutActivity extends BaseActivity {
|
|||
binding.aboutRateUs.setOnClickListener(this::launchRatings);
|
||||
binding.aboutCredits.setOnClickListener(this::launchCredits);
|
||||
binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy);
|
||||
binding.aboutUserGuide.setOnClickListener(this::launchUserGuide);
|
||||
binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions);
|
||||
binding.aboutTranslate.setOnClickListener(this::launchTranslate);
|
||||
}
|
||||
|
|
@ -99,7 +101,14 @@ public class AboutActivity extends BaseActivity {
|
|||
}
|
||||
|
||||
public void launchGithub(View view) {
|
||||
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL));
|
||||
Intent intent;
|
||||
try {
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL));
|
||||
intent.setPackage(Urls.GITHUB_PACKAGE_NAME);
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL));
|
||||
}
|
||||
}
|
||||
|
||||
public void launchWebsite(View view) {
|
||||
|
|
@ -114,6 +123,10 @@ public class AboutActivity extends BaseActivity {
|
|||
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL));
|
||||
}
|
||||
|
||||
public void launchUserGuide(View view) {
|
||||
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL));
|
||||
}
|
||||
|
||||
public void launchPrivacyPolicy(View view) {
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
||||
}
|
||||
|
|
@ -155,17 +168,20 @@ public class AboutActivity extends BaseActivity {
|
|||
spinner.setAdapter(languageAdapter);
|
||||
spinner.setGravity(17);
|
||||
spinner.setPadding(50,0,0,0);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(AboutActivity.this);
|
||||
builder.setView(spinner);
|
||||
builder.setTitle(R.string.about_translate_title)
|
||||
.setMessage(R.string.about_translate_message)
|
||||
.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> {
|
||||
String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition());
|
||||
Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode));
|
||||
});
|
||||
builder.setNegativeButton(R.string.about_translate_cancel, (dialog, which) -> dialog.cancel());
|
||||
builder.create().show();
|
||||
|
||||
Runnable positiveButtonRunnable = () -> {
|
||||
String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition());
|
||||
Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode));
|
||||
};
|
||||
DialogUtil.showAlertDialog(this,
|
||||
getString(R.string.about_translate_title),
|
||||
getString(R.string.about_translate_message),
|
||||
getString(R.string.about_translate_proceed),
|
||||
getString(R.string.about_translate_cancel),
|
||||
positiveButtonRunnable,
|
||||
() -> {},
|
||||
spinner
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
63
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
63
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,10 @@ object BetaConstants {
|
|||
* production server where beta server does not work
|
||||
*/
|
||||
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
||||
|
||||
/**
|
||||
* Commons production's depicts property which is used in beta for some specific GET calls on
|
||||
* production server where beta server does not work
|
||||
*/
|
||||
const val DEPICTS_PROPERTY = "P180"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||
override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
|
||||
|
||||
override fun newArray(size: Int): Array<CameraPosition?> = 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
|
||||
import static org.acra.ReportField.ANDROID_VERSION;
|
||||
import static org.acra.ReportField.APP_VERSION_CODE;
|
||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
||||
import static org.acra.ReportField.PHONE_MODEL;
|
||||
import static org.acra.ReportField.STACK_TRACE;
|
||||
import static org.acra.ReportField.USER_COMMENT;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
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.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.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
|
||||
import fr.free.nrw.commons.concurrency.ThreadPoolService;
|
||||
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.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 io.reactivex.Completable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.internal.functions.Functions;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.acra.ACRA;
|
||||
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(
|
||||
buildConfigClass = BuildConfig.class,
|
||||
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
|
||||
reportFormat = StringFormat.KEY_VALUE_LIST,
|
||||
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL,
|
||||
STACK_TRACE}
|
||||
)
|
||||
|
||||
@AcraMailSender(
|
||||
mailTo = "commons-app-android-private@googlegroups.com",
|
||||
reportAsFile = false
|
||||
)
|
||||
|
||||
@AcraDialog(
|
||||
resTheme = R.style.Theme_AppCompat_Dialog,
|
||||
resText = R.string.crash_dialog_text,
|
||||
resTitle = R.string.crash_dialog_title,
|
||||
resCommentPrompt = R.string.crash_dialog_comment_prompt
|
||||
)
|
||||
|
||||
public class CommonsApplication extends MultiDexApplication {
|
||||
|
||||
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
@Inject
|
||||
DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore defaultPrefs;
|
||||
|
||||
@Inject
|
||||
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
|
||||
|
||||
/**
|
||||
* Constants begin
|
||||
*/
|
||||
public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001;
|
||||
|
||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
|
||||
|
||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||
|
||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback";
|
||||
|
||||
public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll";
|
||||
|
||||
public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --";
|
||||
|
||||
/**
|
||||
* Constants End
|
||||
*/
|
||||
|
||||
private RefWatcher refWatcher;
|
||||
|
||||
private static CommonsApplication INSTANCE;
|
||||
|
||||
public static CommonsApplication getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private AppLanguageLookUpTable languageLookUpTable;
|
||||
|
||||
public AppLanguageLookUpTable getLanguageLookUpTable() {
|
||||
return languageLookUpTable;
|
||||
}
|
||||
|
||||
@Inject
|
||||
ContributionDao contributionDao;
|
||||
|
||||
/**
|
||||
* In memory list of contributios whose uploads ahve been paused by the user
|
||||
*/
|
||||
public static Map<String, Boolean> pauseUploads = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
INSTANCE = this;
|
||||
ACRA.init(this);
|
||||
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token));
|
||||
|
||||
ApplicationlessInjection
|
||||
.getInstance(this)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
|
||||
|
||||
initTimber();
|
||||
|
||||
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
||||
Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
|
||||
if (null == defaultExifTagsSet) {
|
||||
defaultExifTagsSet = new HashSet<>();
|
||||
}
|
||||
defaultExifTagsSet.add(getString(R.string.exif_tag_location));
|
||||
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet);
|
||||
}
|
||||
|
||||
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
||||
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
|
||||
.setNetworkFetcher(customOkHttpNetworkFetcher)
|
||||
.setDownsampleEnabled(true)
|
||||
.build();
|
||||
try {
|
||||
Fresco.initialize(this, config);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
// TODO: Remove when we're able to initialize Fresco in test builds.
|
||||
}
|
||||
|
||||
createNotificationChannel(this);
|
||||
|
||||
languageLookUpTable = new AppLanguageLookUpTable(this);
|
||||
|
||||
// This handler will catch exceptions thrown from Observables after they are disposed,
|
||||
// or from Observables that are (deliberately or not) missing an onError handler.
|
||||
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());
|
||||
|
||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||
return;
|
||||
}
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
|
||||
*/
|
||||
private void initTimber() {
|
||||
boolean isBeta = ConfigUtils.isBetaFlavour();
|
||||
String logFileName =
|
||||
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
|
||||
String logDirectory = LogUtils.getLogDirectory();
|
||||
//Delete stale logs if they have exceeded the specified size
|
||||
deleteStaleLogs(logFileName, logDirectory);
|
||||
|
||||
FileLoggingTree tree = new FileLoggingTree(
|
||||
Log.VERBOSE,
|
||||
logFileName,
|
||||
logDirectory,
|
||||
1000,
|
||||
getFileLoggingThreadPool());
|
||||
|
||||
Timber.plant(tree);
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the logs zip file at the specified directory and file locations specified in the
|
||||
* params
|
||||
*
|
||||
* @param logFileName
|
||||
* @param logDirectory
|
||||
*/
|
||||
private void deleteStaleLogs(String logFileName, String logDirectory) {
|
||||
try {
|
||||
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
|
||||
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
|
||||
file.delete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isRoboUnitTest() {
|
||||
return "robolectric".equals(Build.FINGERPRINT);
|
||||
}
|
||||
|
||||
private ThreadPoolService getFileLoggingThreadPool() {
|
||||
return new ThreadPoolService.Builder("file-logging-thread")
|
||||
.setPriority(Process.THREAD_PRIORITY_LOWEST)
|
||||
.setPoolSize(1)
|
||||
.setExceptionHandler(new BackgroundPoolExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void createNotificationChannel(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager manager = (NotificationManager) context
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = manager
|
||||
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
|
||||
if (channel == null) {
|
||||
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
|
||||
context.getString(R.string.notifications_channel_name_all),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
|
||||
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps in setting up LeakCanary library
|
||||
*
|
||||
* @return instance of LeakCanary
|
||||
*/
|
||||
protected RefWatcher setupLeakCanary() {
|
||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
return RefWatcher.DISABLED;
|
||||
}
|
||||
return LeakCanary.install(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way to get member refWatcher
|
||||
*
|
||||
* @param context Application context
|
||||
* @return application member refWatcher
|
||||
*/
|
||||
public static RefWatcher getRefWatcher(Context context) {
|
||||
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* clears data of current application
|
||||
*
|
||||
* @param context Application context
|
||||
* @param logoutListener Implementation of interface LogoutListener
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
||||
File cacheDirectory = context.getCacheDir();
|
||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
||||
if (applicationDirectory.exists()) {
|
||||
String[] fileNames = applicationDirectory.list();
|
||||
for (String fileName : fileNames) {
|
||||
if (!fileName.equals("lib")) {
|
||||
FileUtils.deleteFile(new File(applicationDirectory, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionManager.logout()
|
||||
.andThen(Completable.fromAction(() -> {
|
||||
Timber.d("All accounts have been removed");
|
||||
clearImageCache();
|
||||
//TODO: fix preference manager
|
||||
defaultPrefs.clearAll();
|
||||
defaultPrefs.putBoolean("firstrun", false);
|
||||
updateAllDatabases();
|
||||
}
|
||||
))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(logoutListener::onLogoutComplete, Timber::e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all images cache held by Fresco
|
||||
*/
|
||||
private void clearImageCache() {
|
||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||
imagePipeline.clearCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tables and re-creates them.
|
||||
*/
|
||||
private void updateAllDatabases() {
|
||||
dbOpenHelper.getReadableDatabase().close();
|
||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
CategoryDao.Table.onDelete(db);
|
||||
dbOpenHelper.deleteTable(db,
|
||||
CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
|
||||
|
||||
try {
|
||||
contributionDao.deleteAll();
|
||||
} catch (SQLiteException e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
BookmarkPicturesDao.Table.onDelete(db);
|
||||
BookmarkLocationsDao.Table.onDelete(db);
|
||||
Table.onDelete(db);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface used to get log-out events
|
||||
*/
|
||||
public interface LogoutListener {
|
||||
|
||||
void onLogoutComplete();
|
||||
}
|
||||
}
|
||||
414
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
414
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
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.SQLiteException
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.imagepipeline.core.ImagePipelineConfig
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
||||
import fr.free.nrw.commons.category.CategoryDao
|
||||
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
|
||||
import fr.free.nrw.commons.concurrency.ThreadPoolService
|
||||
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.getVersionNameWithSha
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.internal.functions.Functions
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.acra.ACRA.init
|
||||
import org.acra.ReportField
|
||||
import org.acra.annotation.AcraCore
|
||||
import org.acra.annotation.AcraDialog
|
||||
import org.acra.annotation.AcraMailSender
|
||||
import org.acra.data.StringFormat
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@AcraCore(
|
||||
buildConfigClass = BuildConfig::class,
|
||||
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
|
||||
reportFormat = StringFormat.KEY_VALUE_LIST,
|
||||
reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE]
|
||||
)
|
||||
|
||||
@AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false)
|
||||
|
||||
@AcraDialog(
|
||||
resTheme = R.style.Theme_AppCompat_Dialog,
|
||||
resText = R.string.crash_dialog_text,
|
||||
resTitle = R.string.crash_dialog_title,
|
||||
resCommentPrompt = R.string.crash_dialog_comment_prompt
|
||||
)
|
||||
|
||||
class CommonsApplication : MultiDexApplication() {
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
@Inject
|
||||
lateinit var dbOpenHelper: DBOpenHelper
|
||||
|
||||
@Inject
|
||||
@field:Named("default_preferences")
|
||||
lateinit var defaultPrefs: JsonKvStore
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: CommonsCookieJar
|
||||
|
||||
@Inject
|
||||
lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher
|
||||
|
||||
var languageLookUpTable: AppLanguageLookUpTable? = null
|
||||
private set
|
||||
|
||||
@Inject
|
||||
lateinit var contributionDao: ContributionDao
|
||||
|
||||
/**
|
||||
* Used to declare and initialize various components and dependencies
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
instance = this
|
||||
init(this)
|
||||
|
||||
ApplicationlessInjection
|
||||
.getInstance(this)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
|
||||
initTimber()
|
||||
|
||||
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
||||
var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS)
|
||||
if (null == defaultExifTagsSet) {
|
||||
defaultExifTagsSet = HashSet()
|
||||
}
|
||||
defaultExifTagsSet.add(getString(R.string.exif_tag_location))
|
||||
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet)
|
||||
}
|
||||
|
||||
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
||||
val config = ImagePipelineConfig.newBuilder(this)
|
||||
.setNetworkFetcher(customOkHttpNetworkFetcher)
|
||||
.setDownsampleEnabled(true)
|
||||
.build()
|
||||
try {
|
||||
Fresco.initialize(this, config)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
// TODO: Remove when we're able to initialize Fresco in test builds.
|
||||
}
|
||||
|
||||
createNotificationChannel(this)
|
||||
|
||||
languageLookUpTable = AppLanguageLookUpTable(this)
|
||||
|
||||
// This handler will catch exceptions thrown from Observables after they are disposed,
|
||||
// or from Observables that are (deliberately or not) missing an onError handler.
|
||||
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer())
|
||||
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0")
|
||||
}
|
||||
|
||||
/**
|
||||
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
|
||||
*/
|
||||
private fun initTimber() {
|
||||
val isBeta = isBetaFlavour
|
||||
val logFileName =
|
||||
if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs"
|
||||
val logDirectory = LogUtils.getLogDirectory()
|
||||
//Delete stale logs if they have exceeded the specified size
|
||||
deleteStaleLogs(logFileName, logDirectory)
|
||||
|
||||
val tree = FileLoggingTree(
|
||||
Log.VERBOSE,
|
||||
logFileName,
|
||||
logDirectory,
|
||||
1000,
|
||||
fileLoggingThreadPool
|
||||
)
|
||||
|
||||
Timber.plant(tree)
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the logs zip file at the specified directory and file locations specified in the
|
||||
* params
|
||||
*
|
||||
* @param logFileName
|
||||
* @param logDirectory
|
||||
*/
|
||||
private fun deleteStaleLogs(logFileName: String, logDirectory: String) {
|
||||
try {
|
||||
val file = File("$logDirectory/zip/$logFileName.zip")
|
||||
if (file.exists() && file.totalSpace > 1000000) { // In Kbs
|
||||
file.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
private val fileLoggingThreadPool: ThreadPoolService
|
||||
get() = ThreadPoolService.Builder("file-logging-thread")
|
||||
.setPriority(Process.THREAD_PRIORITY_LOWEST)
|
||||
.setPoolSize(1)
|
||||
.setExceptionHandler(BackgroundPoolExceptionHandler())
|
||||
.build()
|
||||
|
||||
val userAgent: String
|
||||
get() = ("Commons/" + this.getVersionNameWithSha()
|
||||
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE)
|
||||
|
||||
/**
|
||||
* clears data of current application
|
||||
*
|
||||
* @param context Application context
|
||||
* @param logoutListener Implementation of interface LogoutListener
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
fun clearApplicationData(context: Context, logoutListener: LogoutListener) {
|
||||
val cacheDirectory = context.cacheDir
|
||||
val applicationDirectory = File(cacheDirectory.parent)
|
||||
if (applicationDirectory.exists()) {
|
||||
val fileNames = applicationDirectory.list()
|
||||
for (fileName in fileNames) {
|
||||
if (fileName != "lib") {
|
||||
FileUtils.deleteFile(File(applicationDirectory, fileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionManager.logout()
|
||||
.andThen(Completable.fromAction { cookieJar.clear() })
|
||||
.andThen(Completable.fromAction {
|
||||
Timber.d("All accounts have been removed")
|
||||
clearImageCache()
|
||||
//TODO: fix preference manager
|
||||
defaultPrefs.clearAll()
|
||||
defaultPrefs.putBoolean("firstrun", false)
|
||||
updateAllDatabases()
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all images cache held by Fresco
|
||||
*/
|
||||
private fun clearImageCache() {
|
||||
val imagePipeline = Fresco.getImagePipeline()
|
||||
imagePipeline.clearCaches()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tables and re-creates them.
|
||||
*/
|
||||
private fun updateAllDatabases() {
|
||||
dbOpenHelper.readableDatabase.close()
|
||||
val db = dbOpenHelper.writableDatabase
|
||||
|
||||
CategoryDao.Table.onDelete(db)
|
||||
dbOpenHelper.deleteTable(
|
||||
db,
|
||||
DBOpenHelper.CONTRIBUTIONS_TABLE
|
||||
) //Delete the contributions table in the existing db on older versions
|
||||
|
||||
try {
|
||||
contributionDao.deleteAll()
|
||||
} catch (e: SQLiteException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
BookmarkPicturesDao.Table.onDelete(db)
|
||||
BookmarkLocationsDao.Table.onDelete(db)
|
||||
BookmarkItemsDao.Table.onDelete(db)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface used to get log-out events
|
||||
*/
|
||||
interface LogoutListener {
|
||||
fun 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.
|
||||
*/
|
||||
open class BaseLogoutListener : LogoutListener {
|
||||
var ctx: Context
|
||||
var loginMessage: String? = null
|
||||
var userName: String? = null
|
||||
|
||||
/**
|
||||
* Constructor for BaseLogoutListener.
|
||||
*
|
||||
* @param ctx Application context
|
||||
*/
|
||||
constructor(ctx: Context) {
|
||||
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
|
||||
*/
|
||||
constructor(
|
||||
ctx: Context, loginMessage: String?,
|
||||
loginUsername: String?
|
||||
) {
|
||||
this.ctx = ctx
|
||||
this.loginMessage = loginMessage
|
||||
this.userName = loginUsername
|
||||
}
|
||||
|
||||
override fun onLogoutComplete() {
|
||||
Timber.d("Logout complete callback received.")
|
||||
val loginIntent = Intent(ctx, LoginActivity::class.java)
|
||||
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
if (loginMessage != null) {
|
||||
loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage)
|
||||
}
|
||||
if (userName != null) {
|
||||
loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, 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.
|
||||
*/
|
||||
class ActivityLogoutListener : BaseLogoutListener {
|
||||
var 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.
|
||||
*/
|
||||
constructor(activity: Activity, ctx: Context) : 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.
|
||||
*/
|
||||
constructor(
|
||||
activity: Activity, ctx: Context?,
|
||||
loginMessage: String?, loginUsername: String?
|
||||
) : super(activity, loginMessage, loginUsername) {
|
||||
this.activity = activity
|
||||
}
|
||||
|
||||
override fun onLogoutComplete() {
|
||||
super.onLogoutComplete()
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage"
|
||||
const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername"
|
||||
|
||||
const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled"
|
||||
|
||||
/**
|
||||
* Constants begin
|
||||
*/
|
||||
const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001
|
||||
|
||||
const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]"
|
||||
|
||||
const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com"
|
||||
|
||||
const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback"
|
||||
|
||||
const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com"
|
||||
|
||||
const val REPORT_EMAIL_SUBJECT: String = "Report a violation"
|
||||
|
||||
const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll"
|
||||
|
||||
const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --"
|
||||
|
||||
/**
|
||||
* Constants End
|
||||
*/
|
||||
|
||||
@JvmStatic
|
||||
lateinit var instance: CommonsApplication
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
var isPaused: Boolean = false
|
||||
|
||||
@JvmStatic
|
||||
fun createNotificationChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = context
|
||||
.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
var channel = manager
|
||||
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL)
|
||||
if (channel == null) {
|
||||
channel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID_ALL,
|
||||
context.getString(R.string.notifications_channel_name_all),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
|
||||
/**
|
||||
* Helper class for starting the activity
|
||||
*/
|
||||
public final class LocationPicker {
|
||||
|
||||
/**
|
||||
* Getting camera position from the intent using constants
|
||||
*
|
||||
* @param data intent
|
||||
* @return CameraPosition
|
||||
*/
|
||||
public static CameraPosition getCameraPosition(final Intent data) {
|
||||
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
||||
}
|
||||
|
||||
public static class IntentBuilder {
|
||||
|
||||
private final Intent intent;
|
||||
|
||||
/**
|
||||
* Creates a new builder that creates an intent to launch the place picker activity.
|
||||
*/
|
||||
public IntentBuilder() {
|
||||
intent = new Intent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and puts location in intent
|
||||
* @param position CameraPosition
|
||||
* @return LocationPicker.IntentBuilder
|
||||
*/
|
||||
public LocationPicker.IntentBuilder defaultLocation(
|
||||
final CameraPosition position) {
|
||||
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and puts activity name in intent
|
||||
* @param activity activity key
|
||||
* @return LocationPicker.IntentBuilder
|
||||
*/
|
||||
public LocationPicker.IntentBuilder activityKey(
|
||||
final String activity) {
|
||||
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the activity
|
||||
* @param activity Activity
|
||||
* @return Intent
|
||||
*/
|
||||
public Intent build(final Activity activity) {
|
||||
intent.setClass(activity, LocationPickerActivity.class);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
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 android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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 com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.mapbox.android.core.permissions.PermissionsManager;
|
||||
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.modes.CameraMode;
|
||||
import com.mapbox.mapboxsdk.location.modes.RenderMode;
|
||||
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.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
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> {
|
||||
|
||||
/**
|
||||
* DROPPED_MARKER_LAYER_ID : id for layer
|
||||
*/
|
||||
private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID";
|
||||
/**
|
||||
* cameraPosition : position of picker
|
||||
*/
|
||||
private CameraPosition cameraPosition;
|
||||
/**
|
||||
* markerImage : picker image
|
||||
*/
|
||||
private ImageView markerImage;
|
||||
/**
|
||||
* mapboxMap : map
|
||||
*/
|
||||
private MapboxMap mapboxMap;
|
||||
/**
|
||||
* mapView : view of the map
|
||||
*/
|
||||
private MapView mapView;
|
||||
/**
|
||||
* tvAttribution : credit
|
||||
*/
|
||||
private AppCompatTextView tvAttribution;
|
||||
/**
|
||||
* activity : activity key
|
||||
*/
|
||||
private String activity;
|
||||
/**
|
||||
* modifyLocationButton : button for start editing location
|
||||
*/
|
||||
Button modifyLocationButton;
|
||||
/**
|
||||
* showInMapButton : button for showing in map
|
||||
*/
|
||||
TextView showInMapButton;
|
||||
/**
|
||||
* placeSelectedButton : fab for selecting location
|
||||
*/
|
||||
FloatingActionButton placeSelectedButton;
|
||||
/**
|
||||
* droppedMarkerLayer : Layer for static screen
|
||||
*/
|
||||
private Layer droppedMarkerLayer;
|
||||
/**
|
||||
* shadow : imageview of shadow
|
||||
*/
|
||||
private ImageView shadow;
|
||||
/**
|
||||
* largeToolbarText : textView of shadow
|
||||
*/
|
||||
private TextView largeToolbarText;
|
||||
/**
|
||||
* smallToolbarText : textView of shadow
|
||||
*/
|
||||
private TextView smallToolbarText;
|
||||
/**
|
||||
* applicationKvStore : for storing values
|
||||
*/
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
public
|
||||
JsonKvStore applicationKvStore;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.hide();
|
||||
}
|
||||
setContentView(R.layout.activity_location_picker);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
cameraPosition = getIntent()
|
||||
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
||||
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
|
||||
}
|
||||
|
||||
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
|
||||
.get(LocationPickerViewModel.class);
|
||||
viewModel.getResult().observe(this, this);
|
||||
|
||||
bindViews();
|
||||
addBackButtonListener();
|
||||
addPlaceSelectedButton();
|
||||
addCredits();
|
||||
getToolbarUI();
|
||||
|
||||
if (activity.equals("UploadActivity")) {
|
||||
placeSelectedButton.setVisibility(View.GONE);
|
||||
modifyLocationButton.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));
|
||||
}
|
||||
|
||||
mapView.onCreate(savedInstanceState);
|
||||
mapView.getMapAsync(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* For showing credits
|
||||
*/
|
||||
private void addCredits() {
|
||||
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
||||
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking back button destroy locationPickerActivity
|
||||
*/
|
||||
private void addBackButtonListener() {
|
||||
final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button);
|
||||
backButton.setOnClickListener(view -> finish());
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds mapView and location picker icon
|
||||
*/
|
||||
private void bindViews() {
|
||||
mapView = findViewById(R.id.map_view);
|
||||
markerImage = findViewById(R.id.location_picker_image_view_marker);
|
||||
tvAttribution = findViewById(R.id.tv_attribution);
|
||||
modifyLocationButton = findViewById(R.id.modify_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
|
||||
*/
|
||||
private void getToolbarUI() {
|
||||
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
|
||||
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view);
|
||||
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view);
|
||||
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(Style.MAPBOX_STREETS, style -> {
|
||||
|
||||
if (modifyLocationButton.getVisibility() == View.VISIBLE) {
|
||||
initDroppedMarker(style);
|
||||
adjustCameraBasedOnOptions();
|
||||
enableLocationComponent(style);
|
||||
if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) {
|
||||
final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id");
|
||||
if (source != null) {
|
||||
source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(),
|
||||
cameraPosition.target.getLatitude()));
|
||||
}
|
||||
droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID);
|
||||
if (droppedMarkerLayer != null) {
|
||||
droppedMarkerLayer.setProperties(visibility(VISIBLE));
|
||||
markerImage.setVisibility(View.GONE);
|
||||
shadow.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
adjustCameraBasedOnOptions();
|
||||
enableLocationComponent(style);
|
||||
bindListeners();
|
||||
}
|
||||
modifyLocationButton.setOnClickListener(v -> {
|
||||
placeSelectedButton.setVisibility(View.VISIBLE);
|
||||
modifyLocationButton.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();
|
||||
});
|
||||
|
||||
showInMapButton.setOnClickListener(v -> showInMap());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the location in map app
|
||||
*/
|
||||
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)
|
||||
));
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void addPlaceSelectedButton() {
|
||||
placeSelectedButton = findViewById(R.id.location_chosen_button);
|
||||
placeSelectedButton.setOnClickListener(view -> placeSelected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the intent with required data
|
||||
*/
|
||||
void placeSelected() {
|
||||
if (activity.equals("NoLocationUploadActivity")) {
|
||||
applicationKvStore.putString(LAST_LOCATION,
|
||||
mapboxMap.getCameraPosition().target.getLatitude()
|
||||
+ ","
|
||||
+ mapboxMap.getCameraPosition().target.getLongitude());
|
||||
}
|
||||
final Intent returningIntent = new Intent();
|
||||
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
||||
mapboxMap.getCameraPosition());
|
||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mapView.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mapView.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
mapView.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
mapView.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final @NotNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
mapView.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
mapView.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
mapView.onLowMemory();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
/**
|
||||
* Constants need for location picking
|
||||
*/
|
||||
public final class LocationPickerConstants {
|
||||
|
||||
public static final String ACTIVITY_KEY
|
||||
= "location.picker.activity";
|
||||
|
||||
public static final String MAP_CAMERA_POSITION
|
||||
= "location.picker.cameraPosition";
|
||||
|
||||
|
||||
private LocationPickerConstants() {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Observes live camera position data
|
||||
*/
|
||||
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
|
||||
|
||||
/**
|
||||
* Wrapping CameraPosition with MutableLiveData
|
||||
*/
|
||||
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
|
||||
|
||||
/**
|
||||
* Constructor for this class
|
||||
*
|
||||
* @param application Application
|
||||
*/
|
||||
public LocationPickerViewModel(@NonNull final Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses on camera position changing
|
||||
*
|
||||
* @param call Call<CameraPosition>
|
||||
* @param response Response<CameraPosition>
|
||||
*/
|
||||
@Override
|
||||
public void onResponse(final @NotNull Call<CameraPosition> call,
|
||||
final Response<CameraPosition> response) {
|
||||
if (response.body() == null) {
|
||||
result.setValue(null);
|
||||
return;
|
||||
}
|
||||
result.setValue(response.body());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
|
||||
Timber.e(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets live CameraPosition
|
||||
*
|
||||
* @return MutableLiveData<CameraPosition>
|
||||
*/
|
||||
public MutableLiveData<CameraPosition> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
30
app/src/main/java/fr/free/nrw/commons/MapController.java
Normal file
30
app/src/main/java/fr/free/nrw/commons/MapController.java
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MapController {
|
||||
|
||||
/**
|
||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||
*/
|
||||
public class NearbyPlacesInfo {
|
||||
public List<Place> placeList; // List of nearby places
|
||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||
*/
|
||||
public class ExplorePlacesInfo {
|
||||
public List<Place> explorePlaceList; // List of nearby places
|
||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,12 @@ package fr.free.nrw.commons
|
|||
|
||||
import android.os.Parcelable
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.wikipedia.page.PageTitle
|
||||
import java.util.*
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Parcelize
|
||||
class Media constructor(
|
||||
|
|
@ -14,7 +17,6 @@ class Media constructor(
|
|||
*/
|
||||
var pageId: String = UUID.randomUUID().toString(),
|
||||
var thumbUrl: String? = null,
|
||||
|
||||
/**
|
||||
* Gets image URL
|
||||
* @return Image URL
|
||||
|
|
@ -26,16 +28,11 @@ class Media constructor(
|
|||
*/
|
||||
var filename: String? = null,
|
||||
/**
|
||||
* Gets the file description.
|
||||
* Gets or sets the file description.
|
||||
* @return file description as a string
|
||||
*/
|
||||
// monolingual description on input...
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param fallbackDescription the new description of the file
|
||||
*/
|
||||
var fallbackDescription: String? = null,
|
||||
|
||||
/**
|
||||
* Gets the upload date of the file.
|
||||
* Can be null.
|
||||
|
|
@ -43,28 +40,19 @@ class Media constructor(
|
|||
*/
|
||||
var dateUploaded: Date? = null,
|
||||
/**
|
||||
* Gets the license name of the file.
|
||||
* Gets or sets the license name of the file.
|
||||
* @return license as a String
|
||||
*/
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
*
|
||||
* @param license license name as a String
|
||||
*/
|
||||
var license: String? = null,
|
||||
var licenseUrl: String? = null,
|
||||
/**
|
||||
* Gets the name of the creator of the file.
|
||||
* Gets or sets the name of the creator of the file.
|
||||
* @return author name as a String
|
||||
*/
|
||||
/**
|
||||
* Sets the author name of the file.
|
||||
* @param author creator name as a string
|
||||
*/
|
||||
var author: String? = null,
|
||||
|
||||
var user:String?=null,
|
||||
|
||||
var user: String? = null,
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
|
|
@ -77,23 +65,29 @@ class Media constructor(
|
|||
var coordinates: LatLng? = null,
|
||||
var captions: Map<String, String> = emptyMap(),
|
||||
var descriptions: Map<String, String> = emptyMap(),
|
||||
var depictionIds: List<String> = emptyList()
|
||||
var depictionIds: List<String> = emptyList(),
|
||||
/**
|
||||
* This field was added to find non-hidden categories
|
||||
* Stores the mapping of category title to hidden attribute
|
||||
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
||||
*/
|
||||
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
|
||||
) : Parcelable {
|
||||
|
||||
constructor(
|
||||
captions: Map<String, String>,
|
||||
categories: List<String>?,
|
||||
filename: String?,
|
||||
fallbackDescription: String?,
|
||||
author: String?, user:String?
|
||||
author: String?,
|
||||
user: String?,
|
||||
) : this(
|
||||
filename = filename,
|
||||
fallbackDescription = fallbackDescription,
|
||||
dateUploaded = Date(),
|
||||
author = author,
|
||||
user=user,
|
||||
user = user,
|
||||
categories = categories,
|
||||
captions = captions
|
||||
captions = captions,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -102,10 +96,11 @@ class Media constructor(
|
|||
*/
|
||||
val displayTitle: String
|
||||
get() =
|
||||
if (filename != null)
|
||||
if (filename != null) {
|
||||
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
||||
else
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file page title
|
||||
|
|
@ -121,17 +116,21 @@ class Media constructor(
|
|||
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
||||
|
||||
val mostRelevantCaption: String
|
||||
get() = captions[Locale.getDefault().language]
|
||||
?: captions.values.firstOrNull()
|
||||
?: displayTitle
|
||||
get() =
|
||||
captions[Locale.getDefault().language]
|
||||
?: captions.values.firstOrNull()
|
||||
?: displayTitle
|
||||
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
var addedCategories: List<String>? = null
|
||||
// TODO added categories should be removed. It is added for a short fix. On category update,
|
||||
// categories should be re-fetched instead
|
||||
get() = field // getter
|
||||
set(value) { field = value } // setter
|
||||
get() = field // getter
|
||||
set(value) {
|
||||
field = value
|
||||
} // setter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.core.text.HtmlCompat
|
||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||
import fr.free.nrw.commons.media.IdAndCaptions
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -17,42 +17,46 @@ import javax.inject.Singleton
|
|||
* to the media and may change due to editing.
|
||||
*/
|
||||
@Singleton
|
||||
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
|
||||
class MediaDataExtractor
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
) {
|
||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||
mediaClient
|
||||
.getEntities(media.depictionIds)
|
||||
.map {
|
||||
it
|
||||
.entities()
|
||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||
}.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
|
||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||
mediaClient.getEntities(media.depictionIds)
|
||||
.map {
|
||||
it.entities()
|
||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||
}
|
||||
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||
|
||||
fun checkDeletionRequestExists(media: Media) =
|
||||
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||
fun fetchDiscussion(media: Media) =
|
||||
mediaClient
|
||||
.getPageHtml(media.filename!!.replace("File", "File talk"))
|
||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
||||
.onErrorReturn {
|
||||
Timber.d("Error occurred while fetching discussion")
|
||||
""
|
||||
}
|
||||
|
||||
fun fetchDiscussion(media: Media) =
|
||||
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk"))
|
||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
||||
.onErrorReturn {
|
||||
Timber.d("Error occurred while fetching discussion")
|
||||
""
|
||||
}
|
||||
fun refresh(media: Media): Single<Media> =
|
||||
Single.ambArray(
|
||||
mediaClient
|
||||
.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
mediaClient
|
||||
.getMediaSuppressingErrors(media.filename)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
)
|
||||
|
||||
fun refresh(media: Media): Single<Media> {
|
||||
return Single.ambArray(
|
||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
mediaClient.getMedia(media.filename)
|
||||
.onErrorResumeNext { Single.never() }
|
||||
)
|
||||
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
|
||||
|
||||
/**
|
||||
* Fetches wikitext from mediaClient
|
||||
*/
|
||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
|
||||
}
|
||||
|
||||
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
|
||||
|
||||
/**
|
||||
* Fetches wikitext from mediaClient
|
||||
*/
|
||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ package fr.free.nrw.commons
|
|||
internal object Urls {
|
||||
const val NEW_ISSUE_URL = "https://github.com/commons-app/apps-android-commons/issues"
|
||||
const val GITHUB_REPO_URL = "https://github.com/commons-app/apps-android-commons"
|
||||
const val GITHUB_PACKAGE_NAME = "com.github.android"
|
||||
const val WEBSITE_URL = "https://commons-app.github.io"
|
||||
const val CREDITS_URL = "https://github.com/commons-app/apps-android-commons/blob/master/CREDITS"
|
||||
const val USER_GUIDE_URL = "https://commons-app.github.io/docs.html"
|
||||
const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md"
|
||||
const val PLAY_STORE_PREFIX = "market://details?id="
|
||||
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
|
||||
const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language="
|
||||
const val TRANSLATE_WIKI_URL =
|
||||
"https://translatewiki.net/w/i.php?title=Special:Translate" +
|
||||
"&group=commons-android-strings&filter=%21translated&action=translate&language="
|
||||
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
|
||||
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
||||
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
||||
|
|
|
|||
|
|
@ -10,17 +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;
|
||||
|
|
@ -30,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) {
|
||||
|
|
@ -136,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))
|
||||
|
|
@ -243,4 +233,18 @@ public class Utils {
|
|||
return "30 Sep";
|
||||
}
|
||||
|
||||
/***
|
||||
* Function to get the current WLM year
|
||||
* It increments at the start of September in line with the other WLM functions
|
||||
* (No consideration of locales for now)
|
||||
* @param calendar
|
||||
* @return
|
||||
*/
|
||||
public static int getWikiLovesMonumentsYear(Calendar calendar) {
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
if (calendar.get(Calendar.MONTH) < Calendar.SEPTEMBER) {
|
||||
year -= 1;
|
||||
}
|
||||
return year;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,25 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.viewpagerindicator.CirclePageIndicator;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.databinding.ActivityWelcomeBinding;
|
||||
import fr.free.nrw.commons.databinding.PopupForCopyrightBinding;
|
||||
import fr.free.nrw.commons.quiz.QuizActivity;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
|
||||
public class WelcomeActivity extends BaseActivity {
|
||||
|
||||
@BindView(R.id.welcomePager)
|
||||
ViewPager pager;
|
||||
@BindView(R.id.welcomePagerIndicator)
|
||||
CirclePageIndicator indicator;
|
||||
private ActivityWelcomeBinding binding;
|
||||
private PopupForCopyrightBinding copyrightBinding;
|
||||
|
||||
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||
private final WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||
private boolean isQuiz;
|
||||
private AlertDialog.Builder dialogBuilder;
|
||||
private AlertDialog dialog;
|
||||
|
||||
/**
|
||||
* Initialises exiting fields and dependencies
|
||||
|
|
@ -32,12 +27,14 @@ public class WelcomeActivity extends BaseActivity {
|
|||
* @param savedInstanceState WelcomeActivity bundled data
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_welcome);
|
||||
binding = ActivityWelcomeBinding.inflate(getLayoutInflater());
|
||||
final View view = binding.getRoot();
|
||||
setContentView(view);
|
||||
|
||||
if (getIntent() != null) {
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
final Bundle bundle = getIntent().getExtras();
|
||||
if (bundle != null) {
|
||||
isQuiz = bundle.getBoolean("isQuiz");
|
||||
}
|
||||
|
|
@ -47,13 +44,24 @@ public class WelcomeActivity extends BaseActivity {
|
|||
|
||||
// Enable skip button if beta flavor
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
findViewById(R.id.finishTutorialButton).setVisibility(View.VISIBLE);
|
||||
binding.finishTutorialButton.setVisibility(View.VISIBLE);
|
||||
|
||||
dialogBuilder = new AlertDialog.Builder(this);
|
||||
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
|
||||
final View contactPopupView = copyrightBinding.getRoot();
|
||||
dialogBuilder.setView(contactPopupView);
|
||||
dialogBuilder.setCancelable(false);
|
||||
dialog = dialogBuilder.create();
|
||||
dialog.show();
|
||||
|
||||
copyrightBinding.buttonOk.setOnClickListener(v -> dialog.dismiss());
|
||||
}
|
||||
|
||||
ButterKnife.bind(this);
|
||||
binding.welcomePager.setAdapter(adapter);
|
||||
binding.welcomePagerIndicator.setViewPager(binding.welcomePager);
|
||||
|
||||
binding.finishTutorialButton.setOnClickListener(v -> finishTutorial());
|
||||
|
||||
pager.setAdapter(adapter);
|
||||
indicator.setViewPager(pager);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,7 +70,7 @@ public class WelcomeActivity extends BaseActivity {
|
|||
@Override
|
||||
public void onDestroy() {
|
||||
if (isQuiz) {
|
||||
Intent i = new Intent(WelcomeActivity.this, QuizActivity.class);
|
||||
final Intent i = new Intent(this, QuizActivity.class);
|
||||
startActivity(i);
|
||||
}
|
||||
super.onDestroy();
|
||||
|
|
@ -73,8 +81,8 @@ public class WelcomeActivity extends BaseActivity {
|
|||
*
|
||||
* @param context Activity context
|
||||
*/
|
||||
public static void startYourself(Context context) {
|
||||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||
public static void startYourself(final Context context) {
|
||||
final Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||
context.startActivity(welcomeIntent);
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +91,8 @@ public class WelcomeActivity extends BaseActivity {
|
|||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (pager.getCurrentItem() != 0) {
|
||||
pager.setCurrentItem(pager.getCurrentItem() - 1, true);
|
||||
if (binding.welcomePager.getCurrentItem() != 0) {
|
||||
binding.welcomePager.setCurrentItem(binding.welcomePager.getCurrentItem() - 1, true);
|
||||
} else {
|
||||
if (defaultKvStore.getBoolean("firstrun", true)) {
|
||||
finishAffinity();
|
||||
|
|
@ -94,7 +102,6 @@ public class WelcomeActivity extends BaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.finishTutorialButton)
|
||||
public void finishTutorial() {
|
||||
defaultKvStore.putBoolean("firstrun", false);
|
||||
finish();
|
||||
|
|
|
|||
|
|
@ -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,9 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
|
||||
/**
|
||||
* This class acts as a Client to facilitate wiki page editing
|
||||
|
|
@ -13,9 +14,8 @@ import org.wikipedia.csrf.CsrfTokenClient
|
|||
*/
|
||||
class PageEditClient(
|
||||
private val csrfTokenClient: CsrfTokenClient,
|
||||
private val pageEditInterface: PageEditInterface
|
||||
private val pageEditInterface: PageEditInterface,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Replace the content of a wiki page
|
||||
* @param pageTitle Title of the page to edit
|
||||
|
|
@ -23,14 +23,60 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking)
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
fun edit(
|
||||
pageTitle: String,
|
||||
text: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
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> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postCreate(
|
||||
pageTitle,
|
||||
summary,
|
||||
text,
|
||||
"text/x-wiki",
|
||||
"wikitext",
|
||||
true,
|
||||
true,
|
||||
csrfTokenClient.getTokenBlocking(),
|
||||
).map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append text to the end of a wiki page
|
||||
|
|
@ -39,14 +85,22 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking)
|
||||
fun appendEdit(
|
||||
pageTitle: String,
|
||||
appendText: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend text to the beginning of a wiki page
|
||||
|
|
@ -55,14 +109,48 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking)
|
||||
fun prependEdit(
|
||||
pageTitle: String,
|
||||
prependText: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
||||
} catch (throwable: Throwable) {
|
||||
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> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(false)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new labels to Wikibase server of commons
|
||||
|
|
@ -72,24 +160,42 @@ class PageEditClient(
|
|||
* @param value label
|
||||
* @return 1 when the edit was successful
|
||||
*/
|
||||
fun setCaptions(summary: String, title: String,
|
||||
language: String, value: String) : Observable<Int>{
|
||||
return try {
|
||||
pageEditInterface.postCaptions(summary, title, language,
|
||||
value, csrfTokenClient.tokenBlocking).map { it.success }
|
||||
fun setCaptions(
|
||||
summary: String,
|
||||
title: String,
|
||||
language: String,
|
||||
value: String,
|
||||
): Observable<Int> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postCaptions(
|
||||
summary,
|
||||
title,
|
||||
language,
|
||||
value,
|
||||
csrfTokenClient.getTokenBlocking(),
|
||||
).map { it.success }
|
||||
} catch (throwable: Throwable) {
|
||||
Observable.just(0)
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
} else {
|
||||
Observable.just(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whole WikiText of required file
|
||||
* @param title : Name of the file
|
||||
* @return Observable<MwQueryResult>
|
||||
*/
|
||||
fun getCurrentWikiText(title: String): Single<String?> {
|
||||
return pageEditInterface.getWikiText(title).map {
|
||||
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
|
||||
fun getCurrentWikiText(title: String): Single<String?> =
|
||||
pageEditInterface.getWikiText(title).map {
|
||||
it
|
||||
.query()
|
||||
?.pages()
|
||||
?.get(0)
|
||||
?.revisions()
|
||||
?.get(0)
|
||||
?.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
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 fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
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 retrofit2.http.*
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* This interface facilitates wiki commons page editing services to the Networking module
|
||||
|
|
@ -27,13 +32,40 @@ 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,
|
||||
@Field("text") text: String,
|
||||
// NOTE: This csrf shold always be sent as the last field of form data
|
||||
@Field("token") token: String
|
||||
@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>
|
||||
|
||||
/**
|
||||
|
|
@ -47,12 +79,12 @@ 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,
|
||||
@Field("appendtext") appendText: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
|
|
@ -66,24 +98,34 @@ 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,
|
||||
@Field("prependtext") prependText: String,
|
||||
@Field("token") token: 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=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(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
||||
fun postCaptions(
|
||||
@Field("summary") summary: String,
|
||||
@Field("title") title: String,
|
||||
@Field("language") language: String,
|
||||
@Field("value") value: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Entities>
|
||||
|
||||
/**
|
||||
|
|
@ -91,11 +133,8 @@ 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
|
||||
@Query("titles") title: String,
|
||||
): Single<MwQueryResponse?>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF
|
||||
import io.reactivex.Observable
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import org.wikipedia.dataclient.Service
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -15,22 +14,33 @@ import javax.inject.Singleton
|
|||
* Thanks are used by a user to show gratitude to another user for their contributions
|
||||
*/
|
||||
@Singleton
|
||||
class ThanksClient @Inject constructor(
|
||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||
@param:Named("commons-service") private val service: Service
|
||||
) {
|
||||
/**
|
||||
* Thanks a user for a particular revision
|
||||
* @param revisionId The revision ID the user would like to thank someone for
|
||||
* @return if thanks was successfully sent to intended recipient
|
||||
*/
|
||||
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)
|
||||
}
|
||||
class ThanksClient
|
||||
@Inject
|
||||
constructor(
|
||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||
private val service: ThanksInterface,
|
||||
) {
|
||||
/**
|
||||
* Thanks a user for a particular revision
|
||||
* @param revisionId The revision ID the user would like to thank someone for
|
||||
* @return if thanks was successfully sent to intended recipient
|
||||
*/
|
||||
fun thank(revisionId: Long): Observable<Boolean> =
|
||||
try {
|
||||
service
|
||||
.thank(
|
||||
revisionId.toString(), // Rev
|
||||
null, // Log
|
||||
csrfTokenClient.getTokenBlocking(), // Token
|
||||
CommonsApplication.instance.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?>
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class AccountUtil {
|
||||
|
||||
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||
|
||||
public AccountUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
*/
|
||||
@Nullable
|
||||
public static Account account(Context context) {
|
||||
try {
|
||||
Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
||||
if (accounts.length > 0) {
|
||||
return accounts[0];
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUserName(Context context) {
|
||||
Account account = account(context);
|
||||
return account == null ? null : account.name;
|
||||
}
|
||||
|
||||
private static AccountManager accountManager(Context context) {
|
||||
return AccountManager.get(context);
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||
import timber.log.Timber
|
||||
|
||||
const val AUTH_TOKEN_TYPE: String = "CommonsAndroid"
|
||||
|
||||
fun getUserName(context: Context): String? {
|
||||
return account(context)?.name
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun account(context: Context): Account? = try {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
|
||||
if (accounts.isNotEmpty()) accounts[0] else null
|
||||
} catch (e: SecurityException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
|
|
@ -1,496 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AccountAuthenticatorActivity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuInflater;
|
||||
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 androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
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.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 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;
|
||||
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;
|
||||
|
||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
||||
WikiSite commonsWikiSite;
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore applicationKvStore;
|
||||
|
||||
@Inject
|
||||
LoginClient loginClient;
|
||||
|
||||
@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;
|
||||
|
||||
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);
|
||||
ApplicationlessInjection
|
||||
.getInstance(this.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
||||
setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_login);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
usernameEdit.addTextChangedListener(textWatcher);
|
||||
passwordEdit.addTextChangedListener(textWatcher);
|
||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
||||
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
loginCredentials.setText(getString(R.string.login_credential));
|
||||
} else {
|
||||
loginCredentials.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@OnFocusChange(R.id.login_password)
|
||||
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()) {
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
performLogin();
|
||||
return true;
|
||||
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
|
||||
performLogin();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@OnClick(R.id.skip_login)
|
||||
void skipLogin() {
|
||||
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
|
||||
.setMessage(R.string.skip_login_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
dialog.cancel();
|
||||
performSkipLogin();
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
||||
.show();
|
||||
}
|
||||
|
||||
@OnClick(R.id.forgot_password)
|
||||
void forgotPassword() {
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
||||
}
|
||||
|
||||
@OnClick(R.id.about_privacy_policy)
|
||||
void onPrivacyPolicyClicked() {
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
||||
}
|
||||
|
||||
@OnClick(R.id.sign_up_button)
|
||||
void signUp() {
|
||||
Intent intent = new Intent(this, SignupActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (sessionManager.getCurrentAccount() != null
|
||||
&& sessionManager.isUserLoggedIn()) {
|
||||
applicationKvStore.putBoolean("login_skipped", false);
|
||||
startMainActivity();
|
||||
}
|
||||
|
||||
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
||||
performSkipLogin();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
compositeDisposable.clear();
|
||||
try {
|
||||
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
|
||||
if (progressDialog != null && progressDialog.isShowing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
usernameEdit.removeTextChangedListener(textWatcher);
|
||||
passwordEdit.removeTextChangedListener(textWatcher);
|
||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
||||
delegate.onDestroy();
|
||||
if(null!=loginClient) {
|
||||
loginClient.cancel();
|
||||
}
|
||||
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();
|
||||
|
||||
showLoggingProgressBar();
|
||||
doLogin(username, password, twoFactorCode);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void showPasswordResetPrompt() {
|
||||
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is called when user skips the login.
|
||||
* It redirects the user to Explore Activity.
|
||||
*/
|
||||
private void performSkipLogin() {
|
||||
applicationKvStore.putBoolean("login_skipped", true);
|
||||
MainActivity.startYourself(this);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void showLoggingProgressBar() {
|
||||
progressDialog = new ProgressDialog(this);
|
||||
progressDialog.setIndeterminate(true);
|
||||
progressDialog.setTitle(getString(R.string.logging_in_title));
|
||||
progressDialog.setMessage(getString(R.string.logging_in_message));
|
||||
progressDialog.setCanceledOnTouchOutside(false);
|
||||
progressDialog.show();
|
||||
}
|
||||
|
||||
private void onLoginSuccess(LoginResult loginResult) {
|
||||
if (!progressDialog.isShowing()) {
|
||||
// no longer attached to activity!
|
||||
return;
|
||||
}
|
||||
compositeDisposable.clear();
|
||||
sessionManager.setUserLoggedIn(true);
|
||||
AppAdapter.get().updateAccount(loginResult);
|
||||
progressDialog.dismiss();
|
||||
showSuccessAndDismissDialog();
|
||||
startMainActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
delegate.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
delegate.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().setContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
progressDialog.dismiss();
|
||||
twoFactorContainer.setVisibility(VISIBLE);
|
||||
twoFactorEdit.setVisibility(VISIBLE);
|
||||
twoFactorEdit.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);
|
||||
}
|
||||
|
||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
||||
showMessage(resId, R.color.secondaryDarkColor);
|
||||
if (progressDialog != null) {
|
||||
progressDialog.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void showMessageAndCancelDialog(String error) {
|
||||
showMessage(error, R.color.secondaryDarkColor);
|
||||
if (progressDialog != null) {
|
||||
progressDialog.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void showSuccessAndDismissDialog() {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
|
||||
public void startMainActivity() {
|
||||
ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
||||
errorMessage.setText(getString(resId));
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
errorMessageContainer.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private void showMessage(String message, @ColorRes int colorResId) {
|
||||
errorMessage.setText(message);
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
||||
errorMessageContainer.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (delegate == null) {
|
||||
delegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return delegate;
|
||||
}
|
||||
|
||||
private class LoginTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
// if progressDialog is visible during the configuration change then store state as true else false so that
|
||||
// we maintain visibility of progressDailog after configuration change
|
||||
if(progressDialog!=null&&progressDialog.isShowing()) {
|
||||
outState.putBoolean(saveProgressDailog,true);
|
||||
} else {
|
||||
outState.putBoolean(saveProgressDailog,false);
|
||||
}
|
||||
outState.putString(saveErrorMessage,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();
|
||||
}
|
||||
private String getPassword(){
|
||||
return passwordEdit.getText().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
usernameEdit.setText(savedInstanceState.getString(saveUsername));
|
||||
passwordEdit.setText(savedInstanceState.getString(savePassword));
|
||||
if(savedInstanceState.getBoolean(saveProgressDailog)) {
|
||||
performLogin();
|
||||
}
|
||||
String errorMessage=savedInstanceState.getString(saveErrorMessage);
|
||||
if(sessionManager.isUserLoggedIn()) {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
||||
} else {
|
||||
showMessage(errorMessage, R.color.secondaryDarkColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.AccountAuthenticatorActivity
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.core.content.ContextCompat
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||
import fr.free.nrw.commons.auth.login.LoginClient
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.databinding.ActivityLoginBinding
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.utils.AbstractTextWatcher
|
||||
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils
|
||||
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class LoginActivity : AccountAuthenticatorActivity() {
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
@Inject
|
||||
@field:Named("default_preferences")
|
||||
lateinit var applicationKvStore: JsonKvStore
|
||||
|
||||
@Inject
|
||||
lateinit var loginClient: LoginClient
|
||||
|
||||
@Inject
|
||||
lateinit var systemThemeUtils: SystemThemeUtils
|
||||
|
||||
private var binding: ActivityLoginBinding? = null
|
||||
private var progressDialog: ProgressDialog? = null
|
||||
private val textWatcher = AbstractTextWatcher(::onTextChanged)
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
private val delegate: AppCompatDelegate by lazy {
|
||||
AppCompatDelegate.create(this, null)
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ApplicationlessInjection
|
||||
.getInstance(this.applicationContext)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
|
||||
val isDarkTheme = systemThemeUtils.isDeviceInNightMode()
|
||||
setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme)
|
||||
delegate.installViewFactory()
|
||||
delegate.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
with(binding!!) {
|
||||
setContentView(root)
|
||||
|
||||
loginUsername.addTextChangedListener(textWatcher)
|
||||
loginPassword.addTextChangedListener(textWatcher)
|
||||
loginTwoFactor.addTextChangedListener(textWatcher)
|
||||
|
||||
skipLogin.setOnClickListener { skipLogin() }
|
||||
forgotPassword.setOnClickListener { forgotPassword() }
|
||||
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
|
||||
signUpButton.setOnClickListener { signUp() }
|
||||
loginButton.setOnClickListener { performLogin() }
|
||||
loginPassword.setOnEditorActionListener(::onEditorAction)
|
||||
|
||||
loginPassword.onFocusChangeListener =
|
||||
View.OnFocusChangeListener(::onPasswordFocusChanged)
|
||||
|
||||
if (isBetaFlavour) {
|
||||
loginCredentials.text = getString(R.string.login_credential)
|
||||
} else {
|
||||
loginCredentials.visibility = View.GONE
|
||||
}
|
||||
|
||||
intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let {
|
||||
showMessage(it, R.color.secondaryDarkColor)
|
||||
}
|
||||
|
||||
intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let {
|
||||
loginUsername.setText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
delegate.onPostCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) {
|
||||
applicationKvStore.putBoolean("login_skipped", false)
|
||||
startMainActivity()
|
||||
}
|
||||
|
||||
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
||||
performSkipLogin()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
compositeDisposable.clear()
|
||||
try {
|
||||
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
|
||||
if (progressDialog?.isShowing == true) {
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
with(binding!!) {
|
||||
loginUsername.removeTextChangedListener(textWatcher)
|
||||
loginPassword.removeTextChangedListener(textWatcher)
|
||||
loginTwoFactor.removeTextChangedListener(textWatcher)
|
||||
}
|
||||
delegate.onDestroy()
|
||||
loginClient?.cancel()
|
||||
binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
delegate.onStart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
delegate.onStop()
|
||||
}
|
||||
|
||||
override fun onPostResume() {
|
||||
super.onPostResume()
|
||||
delegate.onPostResume()
|
||||
}
|
||||
|
||||
override fun setContentView(view: View, params: ViewGroup.LayoutParams) {
|
||||
delegate.setContentView(view, params)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
NavUtils.navigateUpFromSameTask(this)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
// if progressDialog is visible during the configuration change then store state as true else false so that
|
||||
// we maintain visibility of progressDailog after configuration change
|
||||
if (progressDialog != null && progressDialog!!.isShowing) {
|
||||
outState.putBoolean(saveProgressDailog, true)
|
||||
} else {
|
||||
outState.putBoolean(saveProgressDailog, false)
|
||||
}
|
||||
outState.putString(
|
||||
saveErrorMessage,
|
||||
binding!!.errorMessage.text.toString()
|
||||
) //Save the errorMessage
|
||||
outState.putString(
|
||||
saveUsername,
|
||||
binding!!.loginUsername.text.toString()
|
||||
) // Save the username
|
||||
outState.putString(
|
||||
savePassword,
|
||||
binding!!.loginPassword.text.toString()
|
||||
) // Save the password
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername))
|
||||
binding!!.loginPassword.setText(savedInstanceState.getString(savePassword))
|
||||
if (savedInstanceState.getBoolean(saveProgressDailog)) {
|
||||
performLogin()
|
||||
}
|
||||
val errorMessage = savedInstanceState.getString(saveErrorMessage)
|
||||
if (sessionManager.isUserLoggedIn) {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||
} else {
|
||||
showMessage(errorMessage, R.color.secondaryDarkColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) {
|
||||
if (!hasFocus) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) =
|
||||
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
|
||||
performLogin()
|
||||
true
|
||||
} else false
|
||||
|
||||
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
|
||||
actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||
|
||||
private fun skipLogin() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.skip_login_title)
|
||||
.setMessage(R.string.skip_login_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int ->
|
||||
dialog.cancel()
|
||||
performSkipLogin()
|
||||
}
|
||||
.setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun forgotPassword() =
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
|
||||
|
||||
private fun onPrivacyPolicyClicked() =
|
||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
||||
|
||||
private fun signUp() =
|
||||
startActivity(Intent(this, SignupActivity::class.java))
|
||||
|
||||
@VisibleForTesting
|
||||
fun performLogin() {
|
||||
Timber.d("Login to start!")
|
||||
val username = binding!!.loginUsername.text.toString()
|
||||
val password = binding!!.loginPassword.text.toString()
|
||||
val twoFactorCode = binding!!.loginTwoFactor.text.toString()
|
||||
|
||||
showLoggingProgressBar()
|
||||
loginClient.doLogin(username,
|
||||
password,
|
||||
twoFactorCode,
|
||||
Locale.getDefault().language,
|
||||
object : LoginCallback {
|
||||
override fun success(loginResult: LoginResult) = runOnUiThread {
|
||||
Timber.d("Login Success")
|
||||
progressDialog!!.dismiss()
|
||||
onLoginSuccess(loginResult)
|
||||
}
|
||||
|
||||
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
|
||||
Timber.d("Requesting 2FA prompt")
|
||||
progressDialog!!.dismiss()
|
||||
askUserForTwoFactorAuth()
|
||||
}
|
||||
|
||||
override fun passwordResetPrompt(token: String?) = runOnUiThread {
|
||||
Timber.d("Showing password reset prompt")
|
||||
progressDialog!!.dismiss()
|
||||
showPasswordResetPrompt()
|
||||
}
|
||||
|
||||
override fun error(caught: Throwable) = runOnUiThread {
|
||||
Timber.e(caught)
|
||||
progressDialog!!.dismiss()
|
||||
showMessageAndCancelDialog(caught.localizedMessage ?: "")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showPasswordResetPrompt() =
|
||||
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword))
|
||||
|
||||
/**
|
||||
* This function is called when user skips the login.
|
||||
* It redirects the user to Explore Activity.
|
||||
*/
|
||||
private fun performSkipLogin() {
|
||||
applicationKvStore.putBoolean("login_skipped", true)
|
||||
MainActivity.startYourself(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showLoggingProgressBar() {
|
||||
progressDialog = ProgressDialog(this).apply {
|
||||
isIndeterminate = true
|
||||
setTitle(getString(R.string.logging_in_title))
|
||||
setMessage(getString(R.string.logging_in_message))
|
||||
setCancelable(false)
|
||||
}
|
||||
progressDialog!!.show()
|
||||
}
|
||||
|
||||
private fun onLoginSuccess(loginResult: LoginResult) {
|
||||
compositeDisposable.clear()
|
||||
sessionManager.setUserLoggedIn(true)
|
||||
sessionManager.updateAccount(loginResult)
|
||||
progressDialog!!.dismiss()
|
||||
showSuccessAndDismissDialog()
|
||||
startMainActivity()
|
||||
}
|
||||
|
||||
override fun getMenuInflater(): MenuInflater =
|
||||
delegate.menuInflater
|
||||
|
||||
@VisibleForTesting
|
||||
fun askUserForTwoFactorAuth() {
|
||||
progressDialog!!.dismiss()
|
||||
with(binding!!) {
|
||||
twoFactorContainer.visibility = View.VISIBLE
|
||||
loginTwoFactor.visibility = View.VISIBLE
|
||||
loginTwoFactor.requestFocus()
|
||||
}
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
|
||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun showMessageAndCancelDialog(@StringRes resId: Int) {
|
||||
showMessage(resId, R.color.secondaryDarkColor)
|
||||
progressDialog?.cancel()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun showMessageAndCancelDialog(error: String) {
|
||||
showMessage(error, R.color.secondaryDarkColor)
|
||||
progressDialog?.cancel()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun showSuccessAndDismissDialog() {
|
||||
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun startMainActivity() {
|
||||
startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) {
|
||||
errorMessage.text = getString(resId)
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
|
||||
errorMessageContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) {
|
||||
errorMessage.text = message
|
||||
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
|
||||
errorMessageContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun onTextChanged(text: String) {
|
||||
val enabled =
|
||||
binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 &&
|
||||
(BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE)
|
||||
binding!!.loginButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun startYourself(context: Context) =
|
||||
context.startActivity(Intent(context, LoginActivity::class.java))
|
||||
|
||||
const val saveProgressDailog: String = "ProgressDailog_state"
|
||||
const val saveErrorMessage: String = "errorMessage"
|
||||
const val saveUsername: String = "username"
|
||||
const val savePassword: String = "password"
|
||||
}
|
||||
}
|
||||
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.login.LoginResult;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
|
||||
/**
|
||||
* Manage the current logged in user session.
|
||||
*/
|
||||
@Singleton
|
||||
public class SessionManager {
|
||||
private final Context context;
|
||||
private Account currentAccount; // Unlike a savings account... ;-)
|
||||
private JsonKvStore defaultKvStore;
|
||||
|
||||
@Inject
|
||||
public SessionManager(Context context,
|
||||
@Named("default_preferences") JsonKvStore defaultKvStore) {
|
||||
this.context = context;
|
||||
this.currentAccount = null;
|
||||
this.defaultKvStore = defaultKvStore;
|
||||
}
|
||||
|
||||
private boolean createAccount(@NonNull String userName, @NonNull String password) {
|
||||
Account account = getCurrentAccount();
|
||||
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
|
||||
removeAccount();
|
||||
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
|
||||
return accountManager().addAccountExplicitly(account, password, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void removeAccount() {
|
||||
Account account = getCurrentAccount();
|
||||
if (account != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
accountManager().removeAccountExplicitly(account);
|
||||
} else {
|
||||
//noinspection deprecation
|
||||
accountManager().removeAccount(account, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAccount(LoginResult result) {
|
||||
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
|
||||
if (accountCreated) {
|
||||
setPassword(result.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
private void setPassword(@NonNull String password) {
|
||||
Account account = getCurrentAccount();
|
||||
if (account != null) {
|
||||
accountManager().setPassword(account, password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
*/
|
||||
@Nullable
|
||||
public Account getCurrentAccount() {
|
||||
if (currentAccount == null) {
|
||||
AccountManager accountManager = AccountManager.get(context);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
||||
if (allAccounts.length != 0) {
|
||||
currentAccount = allAccounts[0];
|
||||
}
|
||||
}
|
||||
return currentAccount;
|
||||
}
|
||||
|
||||
public boolean doesAccountExist() {
|
||||
return getCurrentAccount() != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getUserName() {
|
||||
Account account = getCurrentAccount();
|
||||
return account == null ? null : account.name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPassword() {
|
||||
Account account = getCurrentAccount();
|
||||
return account == null ? null : accountManager().getPassword(account);
|
||||
}
|
||||
|
||||
private AccountManager accountManager() {
|
||||
return AccountManager.get(context);
|
||||
}
|
||||
|
||||
public boolean isUserLoggedIn() {
|
||||
return defaultKvStore.getBoolean("isUserLoggedIn", false);
|
||||
}
|
||||
|
||||
void setUserLoggedIn(boolean isLoggedIn) {
|
||||
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
|
||||
}
|
||||
|
||||
public void forceLogin(Context context) {
|
||||
if (context != null) {
|
||||
LoginActivity.startYourself(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Clears existing accounts from account manager
|
||||
* 2. Calls MediaWikiApi's logout function to clear cookies
|
||||
* @return
|
||||
*/
|
||||
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 a corresponding boolean preference
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public boolean getPreference(String key) {
|
||||
return defaultKvStore.getBoolean(key);
|
||||
}
|
||||
}
|
||||
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manage the current logged in user session.
|
||||
*/
|
||||
@Singleton
|
||||
class SessionManager @Inject constructor(
|
||||
private val context: Context,
|
||||
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
|
||||
) {
|
||||
private val accountManager: AccountManager get() = AccountManager.get(context)
|
||||
|
||||
private var _currentAccount: Account? = null // Unlike a savings account... ;-)
|
||||
val currentAccount: Account? get() {
|
||||
if (_currentAccount == null) {
|
||||
val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE)
|
||||
if (allAccounts.isNotEmpty()) {
|
||||
_currentAccount = allAccounts[0]
|
||||
}
|
||||
}
|
||||
return _currentAccount
|
||||
}
|
||||
|
||||
val userName: String?
|
||||
get() = currentAccount?.name
|
||||
|
||||
var password: String?
|
||||
get() = currentAccount?.let { accountManager.getPassword(it) }
|
||||
private set(value) {
|
||||
currentAccount?.let { accountManager.setPassword(it, value) }
|
||||
}
|
||||
|
||||
val isUserLoggedIn: Boolean
|
||||
get() = defaultKvStore.getBoolean("isUserLoggedIn", false)
|
||||
|
||||
fun updateAccount(result: LoginResult) {
|
||||
if (createAccount(result.userName!!, result.password!!)) {
|
||||
password = result.password
|
||||
}
|
||||
}
|
||||
|
||||
fun doesAccountExist(): Boolean =
|
||||
currentAccount != null
|
||||
|
||||
fun setUserLoggedIn(isLoggedIn: Boolean) =
|
||||
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn)
|
||||
|
||||
fun forceLogin(context: Context?) =
|
||||
context?.let { LoginActivity.startYourself(it) }
|
||||
|
||||
fun getPreference(key: String): Boolean =
|
||||
defaultKvStore.getBoolean(key)
|
||||
|
||||
fun logout(): Completable = Completable.fromObservable(
|
||||
Observable.empty<Any>()
|
||||
.doOnComplete {
|
||||
removeAccount()
|
||||
_currentAccount = null
|
||||
}
|
||||
)
|
||||
|
||||
private fun createAccount(userName: String, password: String): Boolean {
|
||||
var account = currentAccount
|
||||
if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) {
|
||||
removeAccount()
|
||||
account = Account(userName, ACCOUNT_TYPE)
|
||||
return accountManager.addAccountExplicitly(account, password, null)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun removeAccount() {
|
||||
currentAccount?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
accountManager.removeAccountExplicitly(it)
|
||||
} else {
|
||||
accountManager.removeAccount(it, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SignupActivity extends BaseActivity {
|
||||
|
||||
private WebView webView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Timber.d("Signup Activity started");
|
||||
|
||||
webView = new WebView(this);
|
||||
setContentView(webView);
|
||||
|
||||
webView.setWebViewClient(new MyWebViewClient());
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
/*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
|
||||
trust Wikimedia's site... right?*/
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
|
||||
webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL);
|
||||
}
|
||||
|
||||
private class MyWebViewClient extends WebViewClient {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) {
|
||||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||
Timber.d("Overriding URL %s", url);
|
||||
|
||||
Toast toast = Toast.makeText(SignupActivity.this,
|
||||
R.string.account_created, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
// terminate on task completion.
|
||||
finish();
|
||||
return true;
|
||||
} else {
|
||||
//If user clicks any other links in the webview
|
||||
Timber.d("Not overriding URL, URL is: %s", url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import timber.log.Timber
|
||||
|
||||
class SignupActivity : BaseActivity() {
|
||||
private var webView: WebView? = null
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.d("Signup Activity started")
|
||||
|
||||
webView = WebView(this)
|
||||
with(webView!!) {
|
||||
setContentView(this)
|
||||
webViewClient = MyWebViewClient()
|
||||
// Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
|
||||
// trust Wikimedia's site... right?
|
||||
settings.javaScriptEnabled = true
|
||||
loadUrl(BuildConfig.SIGNUP_LANDING_URL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (webView!!.canGoBack()) {
|
||||
webView!!.goBack()
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
|
||||
if (Build.VERSION.SDK_INT <= 25 &&
|
||||
(resources.configuration.uiMode == applicationContext.resources.configuration.uiMode)
|
||||
) return
|
||||
super.applyOverrideConfiguration(overrideConfiguration)
|
||||
}
|
||||
|
||||
private inner class MyWebViewClient : WebViewClient() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean =
|
||||
if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) {
|
||||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||
Timber.d("Overriding URL %s", url)
|
||||
|
||||
Toast.makeText(
|
||||
this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
// terminate on task completion.
|
||||
finish()
|
||||
true
|
||||
} else {
|
||||
//If user clicks any other links in the webview
|
||||
Timber.d("Not overriding URL, URL is: %s", url)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
|
||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
||||
|
||||
/**
|
||||
* Handles WikiMedia commons account Authentication
|
||||
*/
|
||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY};
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
|
||||
public WikiAccountAuthenticator(@NonNull Context context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Bundle with edited Account Properties
|
||||
*/
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "editProperties");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull String accountType, @Nullable String authTokenType,
|
||||
@Nullable String[] requiredFeatures, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
|
||||
if (!supportedAccountType(accountType)) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "addAccount");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
return addAccount(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "confirmCredentials");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @NonNull String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "getAuthToken");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("test", "updateCredentials");
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @NonNull String[] features)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return BuildConfig.ACCOUNT_TYPE.equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a bundle containing a Parcel
|
||||
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
|
||||
*/
|
||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
|
||||
Account account) throws NetworkErrorException {
|
||||
Bundle result = super.getAccountRemovalAllowed(response, account);
|
||||
|
||||
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
&& !result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
||||
|
||||
if (allowed) {
|
||||
for (String auth : SYNC_AUTHORITIES) {
|
||||
ContentResolver.cancelSync(account, auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.NetworkErrorException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
|
||||
private val SYNC_AUTHORITIES = arrayOf(
|
||||
BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles WikiMedia commons account Authentication
|
||||
*/
|
||||
class WikiAccountAuthenticator(
|
||||
private val context: Context
|
||||
) : AbstractAccountAuthenticator(context) {
|
||||
/**
|
||||
* Provides Bundle with edited Account Properties
|
||||
*/
|
||||
override fun editProperties(
|
||||
response: AccountAuthenticatorResponse,
|
||||
accountType: String
|
||||
) = bundleOf("test" to "editProperties")
|
||||
|
||||
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun addAccount(
|
||||
response: AccountAuthenticatorResponse,
|
||||
accountType: String,
|
||||
authTokenType: String?,
|
||||
requiredFeatures: Array<String>?,
|
||||
options: Bundle?
|
||||
) = if (BuildConfig.ACCOUNT_TYPE == accountType) {
|
||||
addAccount(response)
|
||||
} else {
|
||||
bundleOf("test" to "addAccount")
|
||||
}
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun confirmCredentials(
|
||||
response: AccountAuthenticatorResponse, account: Account, options: Bundle?
|
||||
) = bundleOf("test" to "confirmCredentials")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun getAuthToken(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account,
|
||||
authTokenType: String,
|
||||
options: Bundle?
|
||||
) = bundleOf("test" to "getAuthToken")
|
||||
|
||||
override fun getAuthTokenLabel(authTokenType: String) =
|
||||
if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun updateCredentials(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account,
|
||||
authTokenType: String?,
|
||||
options: Bundle?
|
||||
) = bundleOf("test" to "updateCredentials")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun hasFeatures(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account, features: Array<String>
|
||||
) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false)
|
||||
|
||||
/**
|
||||
* Provides a bundle containing a Parcel
|
||||
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
|
||||
*/
|
||||
private fun addAccount(response: AccountAuthenticatorResponse): Bundle {
|
||||
val intent = Intent(context, LoginActivity::class.java)
|
||||
.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||
return bundleOf(AccountManager.KEY_INTENT to intent)
|
||||
}
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun getAccountRemovalAllowed(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account?
|
||||
): Bundle {
|
||||
val result = super.getAccountRemovalAllowed(response, account)
|
||||
|
||||
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
&& !result.containsKey(AccountManager.KEY_INTENT)
|
||||
) {
|
||||
val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
|
||||
if (allowed) {
|
||||
for (auth in SYNC_AUTHORITIES) {
|
||||
ContentResolver.cancelSync(account, auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||
|
||||
/**
|
||||
* Handles the Auth service of the App, see AndroidManifests for details
|
||||
* (Uses Dagger 2 as injector)
|
||||
*/
|
||||
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
||||
|
||||
@Nullable
|
||||
private AbstractAccountAuthenticator authenticator;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
authenticator = new WikiAccountAuthenticator(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return authenticator == null ? null : authenticator.getIBinder();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService
|
||||
|
||||
/**
|
||||
* Handles the Auth service of the App, see AndroidManifests for details
|
||||
* (Uses Dagger 2 as injector)
|
||||
*/
|
||||
class WikiAccountAuthenticatorService : CommonsDaggerService() {
|
||||
private var authenticator: AbstractAccountAuthenticator? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
authenticator = WikiAccountAuthenticator(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? =
|
||||
authenticator?.iBinder
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package fr.free.nrw.commons.auth.csrf
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||
import fr.free.nrw.commons.auth.login.LoginClient
|
||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
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,12 @@
|
|||
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,14 @@
|
|||
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)
|
||||
}
|
||||
258
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
258
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
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 fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
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)?.getGroups() ?: 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,5 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
class LoginFailedException(
|
||||
message: String?,
|
||||
) : Throwable(message)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.Observable
|
||||
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,61 @@
|
|||
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? = 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)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package fr.free.nrw.commons.bookmarks
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class Bookmark(mediaName: String?, mediaCreator: String?,
|
||||
/**
|
||||
* Modifies the content URI - marking this bookmark as already saved in the database
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
var contentUri: Uri?) {
|
||||
/**
|
||||
* Gets the content URI for this bookmark
|
||||
* @return content URI
|
||||
*/
|
||||
/**
|
||||
* Gets the media name
|
||||
* @return the media name
|
||||
*/
|
||||
val mediaName: String = mediaName ?: ""
|
||||
/**
|
||||
* Gets media creator
|
||||
* @return creator name
|
||||
*/
|
||||
val mediaCreator: String = mediaCreator ?: ""
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -206,10 +203,6 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
|
||||
if (mediaDetails != null) {
|
||||
if (mediaDetails.isVisible()) {
|
||||
if (mediaDetails.backButtonClicked()) {
|
||||
// mediaDetails handled the back clicked , no further action required.
|
||||
return true;
|
||||
}
|
||||
// todo add get list fragment
|
||||
((BookmarkFragment) getParentFragment()).setupTabLayout();
|
||||
ArrayList<Integer> removed = mediaDetails.getRemovedItems();
|
||||
|
|
@ -245,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);
|
||||
|
|
@ -257,4 +250,10 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
|
|||
public void onBackStackChanged() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package fr.free.nrw.commons.bookmarks;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
/**
|
||||
* Data class for handling a bookmark fragment and it title
|
||||
*/
|
||||
public class BookmarkPages {
|
||||
private Fragment page;
|
||||
private String title;
|
||||
|
||||
BookmarkPages(Fragment fragment, String title) {
|
||||
this.title = title;
|
||||
this.page = fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fragment
|
||||
* @return fragment object
|
||||
*/
|
||||
public Fragment getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fragment title
|
||||
* @return title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package fr.free.nrw.commons.bookmarks
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
data class BookmarkPages (
|
||||
val page: Fragment? = null,
|
||||
val title: String? = 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 {
|
||||
|
|
|
|||
|
|
@ -15,25 +15,34 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
|||
/**
|
||||
* Helps to inflate Wikidata Items into Items tab
|
||||
*/
|
||||
class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context) :
|
||||
RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
||||
|
||||
class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
class BookmarkItemsAdapter(
|
||||
val list: List<DepictedItem>,
|
||||
val context: Context,
|
||||
) : RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
||||
class BookmarkItemViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
|
||||
var description: TextView = itemView.findViewById(R.id.description)
|
||||
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
|
||||
var layout : ConstraintLayout = itemView.findViewById(R.id.layout_item)
|
||||
var layout: ConstraintLayout = itemView.findViewById(R.id.layout_item)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkItemViewHolder {
|
||||
val v: View = LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_depictions, parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): BookmarkItemViewHolder {
|
||||
val v: View =
|
||||
LayoutInflater
|
||||
.from(context)
|
||||
.inflate(R.layout.item_depictions, parent, false)
|
||||
return BookmarkItemViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BookmarkItemViewHolder, position: Int) {
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: BookmarkItemViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val depictedItem = list[position]
|
||||
holder.depictsLabel.text = depictedItem.name
|
||||
holder.description.text = depictedItem.description
|
||||
|
|
@ -48,7 +57,5 @@ class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context)
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return list.size
|
||||
}
|
||||
}
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.bookmarks.items;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -134,6 +135,7 @@ public class BookmarkItemsDao {
|
|||
* @param cursor : Object for storing database data
|
||||
* @return DepictedItem
|
||||
*/
|
||||
@SuppressLint("Range")
|
||||
DepictedItem fromCursor(final Cursor cursor) {
|
||||
final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
|
||||
final String description
|
||||
|
|
@ -309,22 +311,18 @@ public class BookmarkItemsDao {
|
|||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 7) {
|
||||
if (from < 18) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
|
||||
if (from == 7) {
|
||||
if (from == 18) {
|
||||
// table added in version 19
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
|
||||
if (from == 8) {
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.bookmarks.locations;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -46,11 +47,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 +127,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;
|
||||
}
|
||||
|
|
@ -146,10 +147,11 @@ public class BookmarkLocationsDao {
|
|||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
@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)));
|
||||
|
|
@ -175,8 +177,8 @@ public class BookmarkLocationsDao {
|
|||
cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel().getText());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel().getIcon());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : "");
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null);
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString());
|
||||
|
|
@ -207,40 +209,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 +310,4 @@ public class BookmarkLocationsDao {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,33 @@
|
|||
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.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
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;
|
||||
|
|
@ -37,6 +35,42 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
@Inject CommonPlaceClickActions commonPlaceClickActions;
|
||||
private PlaceAdapter adapter;
|
||||
|
||||
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
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, cameraPickLauncherForResult);
|
||||
} 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
|
||||
* @return an instance of the fragment
|
||||
|
|
@ -51,25 +85,27 @@ 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,
|
||||
galleryPickLauncherForResult,
|
||||
cameraPickLauncherForResult
|
||||
);
|
||||
recyclerView.setAdapter(adapter);
|
||||
binding.listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -84,17 +120,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data);
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package fr.free.nrw.commons.bookmarks.models
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class Bookmark(
|
||||
mediaName: String?,
|
||||
mediaCreator: String?,
|
||||
/**
|
||||
* Gets or Sets the content URI - marking this bookmark as already saved in the database
|
||||
* @return content URI
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
var contentUri: Uri?,
|
||||
) {
|
||||
/**
|
||||
* Gets the media name
|
||||
* @return the media name
|
||||
*/
|
||||
val mediaName: String = mediaName ?: ""
|
||||
|
||||
/**
|
||||
* Gets media creator
|
||||
* @return creator name
|
||||
*/
|
||||
val mediaCreator: String = mediaCreator ?: ""
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.bookmarks.pictures;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.bookmarks.Bookmark;
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.bookmarks.pictures;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -16,7 +17,7 @@ import javax.inject.Named;
|
|||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.bookmarks.Bookmark;
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
||||
|
||||
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI;
|
||||
|
||||
|
|
@ -150,6 +151,7 @@ public class BookmarkPicturesDao {
|
|||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
@NonNull
|
||||
Bookmark fromCursor(Cursor cursor) {
|
||||
String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME));
|
||||
|
|
|
|||
|
|
@ -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,14 +179,18 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
setAdapter(collection);
|
||||
} else {
|
||||
if (gridAdapter.containsAll(collection)) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,7 +203,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
R.layout.layout_category_images,
|
||||
mediaList
|
||||
);
|
||||
gridView.setAdapter(gridAdapter);
|
||||
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -217,6 +212,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
|
|||
* @return GridView Adapter
|
||||
*/
|
||||
public ListAdapter getAdapter() {
|
||||
return gridView.getAdapter();
|
||||
return binding.bookmarkedPicturesList.getAdapter();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package fr.free.nrw.commons.campaigns
|
||||
|
||||
/**
|
||||
* A data class to hold a campaign
|
||||
*/
|
||||
data class Campaign(var title: String? = null,
|
||||
var description: String? = null,
|
||||
var startDate: String? = null,
|
||||
var endDate: String? = null,
|
||||
var link: String? = null,
|
||||
var isWLMCampaign: Boolean = false)
|
||||
|
|
@ -8,6 +8,7 @@ import com.google.gson.annotations.SerializedName
|
|||
class CampaignConfig {
|
||||
@SerializedName("showOnlyLiveCampaigns")
|
||||
private val showOnlyLiveCampaigns = false
|
||||
|
||||
@SerializedName("sortBy")
|
||||
private val sortBy: String? = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.free.nrw.commons.campaigns
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||
|
||||
/**
|
||||
* Data class to hold the response from the campaigns api
|
||||
|
|
@ -8,7 +9,7 @@ import com.google.gson.annotations.SerializedName
|
|||
class CampaignResponseDTO {
|
||||
@SerializedName("config")
|
||||
val campaignConfig: CampaignConfig? = null
|
||||
|
||||
@SerializedName("campaigns")
|
||||
val campaigns: List<Campaign>? = null
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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