Compare commits
No commits in common. "main" and "v2.6.5" have entirely different histories.
85
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
name: "\U0001F41E Bug report"
|
||||
description: Create a report to help us improve.
|
||||
title: "[Bug]: "
|
||||
type: Bug # Retained to categorize the issue as per organization-level type
|
||||
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: Screenshots
|
||||
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
|
|
@ -1,30 +0,0 @@
|
|||
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
|
|
@ -1,46 +0,0 @@
|
|||
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
|
|
@ -1,13 +0,0 @@
|
|||
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
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,17 +0,0 @@
|
|||
**Description (required)**
|
||||
|
||||
Fixes #INSERT_ISSUE_NUMBER_HERE
|
||||
|
||||
What changes did you make and why?
|
||||
|
||||
**Tests performed (required)**
|
||||
|
||||
Tested {build variant, e.g. ProdDebug} on {name of device or emulator} with API level {API level}.
|
||||
|
||||
**Screenshots (for UI changes only)**
|
||||
|
||||
Need help? See https://support.google.com/android/answer/9075928
|
||||
|
||||
---
|
||||
|
||||
_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._
|
||||
114
.github/workflows/android.yml
vendored
|
|
@ -1,114 +0,0 @@
|
|||
name: Android CI
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run tests and generate APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache packages
|
||||
id: cache-packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-packages-${{ runner.os }}-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
|
||||
restore-keys: gradle-packages-${{ runner.os }}
|
||||
|
||||
- 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@v4
|
||||
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/testBetaDebugUnitTestUnifiedCoverage/testBetaDebugUnitTestUnifiedCoverage.xml" -Z
|
||||
|
||||
- name: Generate betaDebug APK
|
||||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||
|
||||
- name: Upload betaDebug APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: betaDebugAPK
|
||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||
|
||||
- name: Generate prodDebug APK
|
||||
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||
|
||||
- name: Upload prodDebug APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: prodDebugAPK
|
||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||
|
||||
- name: Create and PR number artifact
|
||||
run: |
|
||||
echo "{\"pr_number\": ${{ github.event.pull_request.number || 'null' }}}" > pr_number.json
|
||||
|
||||
- name: Upload PR number artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr_number
|
||||
path: ./pr_number.json
|
||||
41
.github/workflows/build-beta.yml
vendored
|
|
@ -1,41 +0,0 @@
|
|||
name: Build beta only
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- 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: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Set env
|
||||
run: echo "COMMIT_SHA=$(git log -n 1 --format='%h')" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate betaDebug APK
|
||||
run: ./gradlew assembleBetaDebug --stacktrace
|
||||
|
||||
- name: Rename betaDebug APK
|
||||
run: mv app/build/outputs/apk/beta/debug/app-*.apk app/build/outputs/apk/beta/debug/apps-android-commons-betaDebug-$COMMIT_SHA.apk
|
||||
|
||||
- name: Upload betaDebug APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apps-android-commons-betaDebugAPK-${{ env.COMMIT_SHA }}
|
||||
path: app/build/outputs/apk/beta/debug/*.apk
|
||||
retention-days: 30
|
||||
96
.github/workflows/comment_artifacts_on_PR.yml
vendored
|
|
@ -1,96 +0,0 @@
|
|||
name: Comment Artifacts on PR
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Android CI" ]
|
||||
types: [ completed ]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: comment-${{ github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
|
||||
steps:
|
||||
- name: Download and process artifacts
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const runId = context.payload.workflow_run.id;
|
||||
|
||||
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: runId,
|
||||
});
|
||||
|
||||
const prNumberArtifact = allArtifacts.data.artifacts.find(artifact => artifact.name === "pr_number");
|
||||
if (!prNumberArtifact) {
|
||||
console.log("pr_number artifact not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: prNumberArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
|
||||
const { execSync } = require('child_process');
|
||||
execSync('unzip -q pr_number.zip -d ./pr_number/');
|
||||
fs.unlinkSync('pr_number.zip');
|
||||
|
||||
const prData = JSON.parse(fs.readFileSync('./pr_number/pr_number.json', 'utf8'));
|
||||
const prNumber = prData.pr_number;
|
||||
|
||||
if (!prNumber || prNumber === 'null') {
|
||||
console.log("No valid PR number found in pr_number.json. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactsToLink = allArtifacts.data.artifacts.filter(artifact => artifact.name !== "pr_number");
|
||||
if (artifactsToLink.length === 0) {
|
||||
console.log("No artifacts to link found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(prNumber),
|
||||
});
|
||||
|
||||
const oldComments = comments.data.filter(comment =>
|
||||
comment.body.startsWith("✅ Generated APK variants!")
|
||||
);
|
||||
for (const comment of oldComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
console.log(`Deleted old comment ID: ${comment.id}`);
|
||||
};
|
||||
|
||||
const commentBody = `✅ Generated APK variants!\n` +
|
||||
artifactsToLink.map(artifact => {
|
||||
const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
||||
return `- 🤖 [Download ${artifact.name}](${artifactUrl})`;
|
||||
}).join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(prNumber),
|
||||
body: commentBody
|
||||
});
|
||||
25
.gitignore
vendored
|
|
@ -1,13 +1,8 @@
|
|||
# IDEA files
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
/.idea/*
|
||||
app/src/main/gen/*
|
||||
|
||||
# IDEA/Android Studio Ignore exceptions
|
||||
!/.idea/codeStyles/
|
||||
!/.idea/inspectionProfiles/
|
||||
*.ipr
|
||||
.idea
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
|
|
@ -32,19 +27,3 @@ app/gradle/wrapper/gradle-wrapper.jar
|
|||
app/gradlew
|
||||
app/gradlew.bat
|
||||
app/gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# related to release notes
|
||||
app/src/prodRelease/play/release-notes/*
|
||||
|
||||
#related to OpenCV
|
||||
/libraries/opencv/build
|
||||
app/src/main/jniLibs
|
||||
#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at:
|
||||
#https://docs.opencv.org/3.3.0/
|
||||
/libraries/opencv/javadoc/
|
||||
captures/*
|
||||
|
||||
# Test and other output
|
||||
app/jacoco.exec
|
||||
app/CommonsContributions
|
||||
app/.*
|
||||
|
|
|
|||
6
.gitreview
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gerrit]
|
||||
host=gerrit.wikimedia.org
|
||||
port=29418
|
||||
project=apps/android/commons.git
|
||||
defaultbranch=master
|
||||
defaultrebase=0
|
||||
323
.idea/codeStyles/Project.xml
generated
|
|
@ -1,323 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="OTHER_INDENT_OPTIONS">
|
||||
<value>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</value>
|
||||
</option>
|
||||
<JSCodeStyleSettings>
|
||||
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||
</JSCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" module="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<Objective-C>
|
||||
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
|
||||
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
|
||||
<option name="INDENT_CLASS_MEMBERS" value="2" />
|
||||
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
|
||||
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
|
||||
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
|
||||
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
|
||||
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
|
||||
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
|
||||
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
|
||||
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
|
||||
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
|
||||
</Objective-C>
|
||||
<Python>
|
||||
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
|
||||
</Python>
|
||||
<TypeScriptCodeStyleSettings>
|
||||
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<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" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ECMA Script Level 4">
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||
<option name="FOR_BRACE_FORCE" value="3" />
|
||||
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||
<option name="THROWS_KEYWORD_WRAP" value="1" />
|
||||
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||
<option name="WRAP_COMMENTS" value="true" />
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||
<option name="FOR_BRACE_FORCE" value="3" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="RIGHT_MARGIN" value="80" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||
<option name="IF_BRACE_FORCE" value="3" />
|
||||
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||
<option name="FOR_BRACE_FORCE" value="3" />
|
||||
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<option name="RIGHT_MARGIN" value="80" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
|
||||
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
|
||||
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
|
||||
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
|
||||
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
|
||||
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
|
||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||
<option name="ASSIGNMENT_WRAP" value="1" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="PROTO">
|
||||
<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>
|
||||
<codeStyleSettings language="Python">
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="RIGHT_MARGIN" value="80" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="SASS">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="SCSS">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="protobuf">
|
||||
<option name="RIGHT_MARGIN" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
|
|
@ -1,5 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
90
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -1,90 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<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="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="REPORT_VARIABLES" value="true" />
|
||||
<option name="REPORT_PARAMETERS" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="LongLine" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="ignoreObjectMethods" value="true" />
|
||||
<option name="ignoreAnonymousClassMethods" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="NonFinalUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreInMatchingInstanceof" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ProblematicWhitespace" 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="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">
|
||||
<option name="m_ignoreStaticFieldAccesses" value="false" />
|
||||
<option name="m_ignoreStaticMethodCalls" value="false" />
|
||||
<option name="m_ignoreStaticAccessFromStaticContext" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnnecessarilyQualifiedStaticallyImportedElement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreAnnotations" value="true" />
|
||||
</inspection_tool>
|
||||
<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" />
|
||||
</profile>
|
||||
</component>
|
||||
5
.mailmap
|
|
@ -1,5 +0,0 @@
|
|||
# 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>
|
||||
56
.travis.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
language: android
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- w3m
|
||||
|
||||
env:
|
||||
global:
|
||||
- ANDROID_TARGET=android-22
|
||||
- ANDROID_ABI=armeabi-v7a
|
||||
- ADB_INSTALL_TIMEOUT=12 # in minutes
|
||||
|
||||
jdk:
|
||||
# - openjdk8 # not yet available
|
||||
- oraclejdk8
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-26.0.2
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
- ${ANDROID_TARGET}
|
||||
- android-25
|
||||
- android-26
|
||||
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
|
||||
licenses:
|
||||
- 'android-sdk-license-.+'
|
||||
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-audio -no-window -no-boot-anim &
|
||||
- android-wait-for-emulator
|
||||
|
||||
script:
|
||||
- ./gradlew clean check connectedCheck jacocoTestReport --stacktrace
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
after_failure:
|
||||
- echo '*** Debug Unit Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html
|
||||
- echo '*** Connected Test Results ***'
|
||||
- w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
488
CHANGELOG.md
|
|
@ -1,493 +1,5 @@
|
|||
# Wikimedia Commons for Android
|
||||
|
||||
## v6.0.2
|
||||
|
||||
### What's changed
|
||||
* Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard
|
||||
* Links in the "File usages" list are now clickable and will take you to the correct page.
|
||||
* Titles for file usages are now clearer and easier to understand
|
||||
* Bug fixes and stability improvements
|
||||
|
||||
## v6.0.1
|
||||
|
||||
### What's changed
|
||||
* The app now supports Android 15 with an improved user interface
|
||||
* Enhanced Nearby with robust and more reliable labels
|
||||
* Bug fixes and stability improvements
|
||||
|
||||
## v5.6.1
|
||||
|
||||
### What's changed
|
||||
* The app no longer uploads images to Wikidata if one exists already for a given item
|
||||
* File usage displays correctly now
|
||||
* No more infinite circular progress bar on nominating an image for deletion
|
||||
* Enhanced location updates while using GPS
|
||||
* Author/uploader names are now available in Media Details for Commons licensing compliance
|
||||
* Improved usage of popups in Nearby
|
||||
* Bug fixes and stability improvements
|
||||
|
||||
## v5.5.0
|
||||
|
||||
### What's changed
|
||||
* Explore images will now be shown based on the map location and not at your current location
|
||||
* Enhanced Wikidata feedback message
|
||||
* Green labels in Explore map will no longer be hidden by other pins thumbnails
|
||||
* Upload wizard's language drop-down now reflects the language used in the pin label
|
||||
* Users can now pick only one image at a time while using the custom selector
|
||||
* Bug fixes and stability improvements
|
||||
|
||||
## v5.4.1
|
||||
|
||||
### What's changed
|
||||
* Custom picker now detects images that are already available on Commons
|
||||
* Improve credit line in image list
|
||||
* Show place cards with loaded names only in the Nearby list
|
||||
* Fix the error that occurs while loading images in Explore
|
||||
|
||||
## v5.3.0
|
||||
|
||||
### What's changed
|
||||
* Enable EmailAuth support
|
||||
* Explore map images no longer show "Unknown"
|
||||
* Fix crash when removing last two images of multiupload
|
||||
* Mark ❌ for closed locations (P3999) in Nearby
|
||||
* Fix two pin labels staying visible at the same time in Explore map
|
||||
* Refactoring and minor UI improvements
|
||||
|
||||
## v5.2.0
|
||||
|
||||
v5.2.0 boasts several new functionalities like:
|
||||
|
||||
* A new refresh button lets you quickly reload the Nearby map
|
||||
* Bookmarks now support categories
|
||||
* Improved feedback and consistency in the user interface
|
||||
* Bug fixes and performance improvements
|
||||
|
||||
### What's changed
|
||||
* Implement "Refresh" button to clear the cache and reload the Nearby map.
|
||||
* `CommonsApplication` migrate to kotlin & some lint fixes.
|
||||
* Revert back to MainScope for database and UI updates and make database operations thread safe.
|
||||
* Hide edit options for logged-out users in Explore screen.
|
||||
* Introduced a button to delete the current folder in custom selector.
|
||||
* Improve Unique File Name Search.
|
||||
* Migration of several modules from Java to Kotlin.
|
||||
* Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins.
|
||||
* Bug fixes and enhancement of Achievements screen.
|
||||
* Show where file is being used on Commons and other wikis.
|
||||
* Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices.
|
||||
* Make dialogs modal and always show the upload icon.
|
||||
* Fix unintentional deletion of subfolders and non-images by custom selector.
|
||||
* Bookmark categories.
|
||||
* Add pull down to refresh in the Contributions screen.
|
||||
* Fix race condition and lag when loading pin details, faster overlay management.
|
||||
* Show cached pins in Nearby even when internet is unavailable
|
||||
|
||||
Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0).
|
||||
|
||||
|
||||
## v5.1.2
|
||||
|
||||
### What's changed
|
||||
|
||||
* Fix the broken category search in the explore screen
|
||||
|
||||
## v5.1.1
|
||||
|
||||
### What's changed
|
||||
|
||||
* Use Android's new EXIF interface to mitigate security issues in old
|
||||
EXIF interface.
|
||||
* Make the icon that helps view the upload queue always visible as it ensures
|
||||
that the queue accessible at all times.
|
||||
|
||||
## v5.1.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
* Enhanced **upload queue management** in the Commons app for smoother, sequential
|
||||
processing, clearer progress tracking, prevention of stuck or duplicate
|
||||
uploads. As part of this improvement, the "Limited Connection mode" has been
|
||||
removed.
|
||||
* Added an option in "Nearby" feature enabling users to **provide feedback on
|
||||
Wikidata items**. Users can report if an item doesn’t exist, is at a different
|
||||
location, or has other issues, with submissions tagged for easy tracking and
|
||||
updates.
|
||||
* Improved the "Nearby" feature by splitting the query into two parts for faster
|
||||
loading and **better performance, especially in areas with dense amount of
|
||||
places**. This update also resolves issues with pins overlapping place names.
|
||||
* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
|
||||
the app such as adding **"Partial Access" support**. Also includes some minor
|
||||
refactoring, and replacement of deprecated circular progress bars.
|
||||
* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
|
||||
appeared blank** in the Category Details screen. Resolved by optimizing view
|
||||
binding handling in the parent fragments.
|
||||
* Fixed an issue where editing depictions removed all other structured data from
|
||||
images. Now, **only depictions are updated, preserving other associated data**.
|
||||
* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
|
||||
from pictures and ensured "Show in map app" accurately reflects this location.
|
||||
* Fixed navigation **after uploading via Nearby by directing users to the Uploads
|
||||
activity** instead of returning to Nearby, preventing confusion about needing to
|
||||
upload again.
|
||||
|
||||
### Bug fixes and various changes
|
||||
|
||||
* Improved the "Nearby" feature to fetch labels based on the user's preferred
|
||||
language instead of defaulting to English.
|
||||
* Added a legend to the "Nearby" feature indicating pin statuses: red for items
|
||||
without pictures, green for those with pictures, and grey for items being
|
||||
checked. A floating action button now allows users to toggle the legend's
|
||||
visibility.
|
||||
* Fixed an issue where the "Nominate for deletion" option is shown to logged out
|
||||
users, preventing app errors and crashes.
|
||||
* Updated the regex pattern that filters categories with an year in it to also
|
||||
filter the 2020s.
|
||||
* Fix an issue where past depictions were not shown as suggestions, despite
|
||||
being saved correctly.
|
||||
* Fixed an issue in custom image picker where exiting the media preview showed
|
||||
only the first image and cleared selections. Now, previously selected images
|
||||
are restored correctly after exiting the preview. This was contributed.
|
||||
* Fixed an issue in custom image picker where scrolling behavior did not
|
||||
maintain position after exiting fullscreen preview, ensuring users remain at
|
||||
the same point in their image roll unless actioned images are filtered. This
|
||||
was contributed.
|
||||
* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
|
||||
threshold and adding an 800ms debounce for smoother pin updates when the map
|
||||
is moved. Queued searches are now canceled on fragment destruction.
|
||||
* Revised author information retrieval to emphasize the custom author name from
|
||||
the metadata instead of the default registered username.
|
||||
* Enhanced notification classification to properly identify "email" type
|
||||
notifications and prompting users to check their e-mail inbox when such
|
||||
notifications are clicked.
|
||||
* Resolved a bug in the language chooser that incorrectly greyed-out previously
|
||||
selected languages, ensuring only the current language is non-selectable during
|
||||
image upload.
|
||||
* Resolved pin color update issue in "Nearby" feature where the pin colour
|
||||
failed to be updated after a successful image upload.
|
||||
|
||||
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/v5.0.2...v5.1.0).
|
||||
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
|
||||
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||
|
||||
## 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
|
||||
- Added link to wiki explaining how to contribute to WLM through app
|
||||
- Fixed various bugs and crashes
|
||||
|
||||
## v3.1.0
|
||||
- Added Wiki Loves Monuments integration for WLM 2021
|
||||
|
||||
## v3.0.2
|
||||
- Fixed crash when uploading high res image
|
||||
- Fixed crash when viewing images in Explore
|
||||
|
||||
## v3.0.1
|
||||
- Pre-fill desc in Nearby uploads with Wikidata item's label + description
|
||||
- Improved ACRA crash reporting
|
||||
- Fixed various crashes
|
||||
|
||||
## v3.0.0
|
||||
- Added Structured Data to upload workflow, users can now add depicts
|
||||
- Added Leaderboard in Achievements screen
|
||||
- Added to-do system for images with no categories/descriptions or with associated Wikipedia articles that have no pictures
|
||||
- Users can now modify and add categories to their uploads from the media details view
|
||||
- New UI for main screen
|
||||
- Limited connection mode added, users can now pause and resume uploads
|
||||
|
||||
## v2.13.1
|
||||
- Added OpenStreetMap attribution
|
||||
- Fixed various crashes
|
||||
- Fixed SQLite error in Nearby map
|
||||
- Fixed issue with Nearby uploads not being associated with Wikidata p18
|
||||
|
||||
## v2.13.0
|
||||
- New media details UI, ability to zoom and pan around image
|
||||
- Added suggestions for a place that needs photos if user uploads a photo that is near one of them
|
||||
- Modifications and fixes to Nearby filters based on user feedback
|
||||
- Multiple crash and bug fixes
|
||||
|
||||
## v2.12.3
|
||||
- Fixed issue with EXIF data, including coords, being removed from uploads
|
||||
|
||||
## v2.12.2
|
||||
- Fixed crash on startup
|
||||
|
||||
## v2.12.1
|
||||
- Fixed issue with Nearby loading in wrong location
|
||||
- Various crash fixes
|
||||
|
||||
## v2.12.0
|
||||
- Completed codebase overhaul
|
||||
- Added filters for place type and place state to Nearby
|
||||
- Switched to using new data client library, aimed at fixing failed uploads
|
||||
- Fixed 2FA not working
|
||||
- Fixed issues with upload date and deletion notifications
|
||||
|
||||
## v2.11.0
|
||||
- Refactored upload process, explore/media details, and peer review to use MVP architecture
|
||||
- Refactored all AsyncTasks to use RxAndroid
|
||||
- Partial migration to Retrofit
|
||||
- Allow users to remove EXIF tags from their uploads if desired
|
||||
- Multiple crash and bug fixes
|
||||
|
||||
## v2.10.2
|
||||
- Fixed remaining issues with date image taken
|
||||
- Fixed database crash
|
||||
|
||||
## v2.10.1
|
||||
- Fixed "stuck before category selection screen" bug
|
||||
- Fixed notification taps
|
||||
- Fixed crash while uploading images
|
||||
- Fixed crash while loading contributions
|
||||
- Fixed sporadic issue with date image was taken
|
||||
|
||||
## v2.10.0
|
||||
- Added option to search for places that need pictures in any location
|
||||
- Added coordinate check for images submitted via Nearby
|
||||
- Added news about ongoing campaigns
|
||||
- Easy retry for failed uploads
|
||||
- Javadocs for Nearby package
|
||||
- Optimized Nearby query for faster loading
|
||||
- Allow users to dismiss notifications
|
||||
- Various bugfixes for Explore, Notifications and Nearby
|
||||
- Fixed uploads getting stuck in "receiving shared content" phase
|
||||
- Fixed empty notifications bell icon in main screen
|
||||
|
||||
## v2.9.0
|
||||
- New main screen UI with Nearby tab
|
||||
- New upload UI and flow
|
||||
- Multiple uploads
|
||||
- Send Log File revamp
|
||||
- Fixed issues with wrong "image taken" date
|
||||
- Fixed default zoom level in Nearby map
|
||||
- Incremented target SDK to 27, with corresponding notification channel fix
|
||||
- Removed several redundant libraries to reduce bloat
|
||||
|
||||
## v2.8.5
|
||||
- Fixed issues with sporadic upload failures due to wrong mimeType
|
||||
|
||||
## v2.8.4
|
||||
- Hotfix for constant upload crashes for Oreo users
|
||||
|
||||
## v2.8.3
|
||||
- Fixed issues with session tokens not being cleared in 2FA, which should reduce p18 edit failures as well
|
||||
- Fixed crash caused by bug in fetching revert count
|
||||
- Fixed crash potentially caused by Traceur library
|
||||
|
||||
## v2.8.2
|
||||
- Fixed bug with uploads sent via Share being given .jpeg extensions and overwriting files of the same name
|
||||
|
||||
## v2.8.1
|
||||
- Fixed bug with category edits not being sent to server
|
||||
|
||||
## v2.8.0
|
||||
- Fixed failed uploads by modifying auth token
|
||||
- Fixed crashes during upload by storing file temporarily
|
||||
- Added automatic Wikidata p18 edits upon Nearby upload
|
||||
- Added Explore feature to browse other Commons images, including featured images
|
||||
- Added Achievements feature to see current level and upload stats
|
||||
- Added quiz for users with high deletion rates
|
||||
- Added first run tutorial for Nearby
|
||||
- Various small improvements to ShareActivity UI
|
||||
|
||||
## v2.7.2
|
||||
- Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location
|
||||
|
||||
## v2.7.1
|
||||
- Fixed UI and permission issues with Nearby
|
||||
- Fixed issue with My Recent Uploads being empty
|
||||
- Fixed blank category issue when uploading directly from Nearby
|
||||
- Various crash fixes
|
||||
|
||||
## v2.7.0
|
||||
- New Nearby Places UI with direct uploads (and associated category suggestions)
|
||||
- Added two-factor authentication login
|
||||
- Added Notifications activity to display user talk messages
|
||||
- Added real-time location tracking in Nearby
|
||||
- Added "rate us", "translate", and FB link in About
|
||||
- Improvements to UI of navigation drawer, tutorial, media details view, login activity and Settings
|
||||
- Added option to nominate picture for deletion in media details view
|
||||
- Too many bug and crash fixes to mention!
|
||||
|
||||
## v2.6.7
|
||||
- Added null checks to prevent frequent crashes in ModificationsSyncAdapter
|
||||
|
||||
## v2.6.6
|
||||
- Refactored Dagger to fix crashes encountered in production
|
||||
- Fixed "?" displaying in description of Nearby places
|
||||
- Database-related cleanup and tests
|
||||
- Optimized dimens.xml
|
||||
- Fixed issue where map opens with incorrect coordinates
|
||||
|
||||
## v2.6.5 beta
|
||||
- Changed "send log" feature to only send logs to private Google group forum
|
||||
- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
Thanks for considering to contribute to this project! A few guidelines for
|
||||
people who want to contribute their code to this software are documented in
|
||||
[this project's Wiki](https://github.com/commons-app/apps-android-commons/wiki/Contributing-Guidelines).
|
||||
If you're not sure where to start head on to [this wiki page](https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome!).
|
||||
|
||||
Here's a gist of the guidelines,
|
||||
|
||||
1. Make separate commits for logically separate changes
|
||||
|
||||
2. Describe your changes well in the commit message
|
||||
|
||||
The first line of the commit message should be a short description of what has
|
||||
changed. It is also good to prefix the first line with "area: " where the "area"
|
||||
is a filename or identifier for the general area of the code being modified.
|
||||
The body should provide a meaningful commit message.
|
||||
|
||||
1. Write Javadocs
|
||||
|
||||
We require contributors to include Javadocs for all new methods and classes
|
||||
submitted via PRs (after 1 May 2018). This is aimed at making it easier for
|
||||
new contributors to dive into our codebase, especially those who are new to
|
||||
Android development. A few things to note:
|
||||
|
||||
- This should not replace the need for code that is easily-readable in
|
||||
and of itself
|
||||
- Please make sure that your Javadocs are reasonably descriptive, not just
|
||||
a copy of the method name
|
||||
- Please do not use `@author` tags - we aim for collective code ownership,
|
||||
and if needed, Git allows us to see who wrote something without needing
|
||||
to add these tags (`git blame`)
|
||||
|
||||
2. Write tests for your code (if possible)
|
||||
|
||||
3. Make sure the Wiki pages don't become stale by updating them (if needed)
|
||||
|
||||
### Further reading
|
||||
|
||||
* [Importance of good commit messages](https://blog.oozou.com/commit-messages-matter-60309983c227?gi=c550a10d0f67)
|
||||
969
CREDITS
|
|
@ -30,979 +30,12 @@ their contribution to the product.
|
|||
* Bruke Mekuria Mulugeta
|
||||
* Paul Hawke
|
||||
* Vishan Seru
|
||||
* Abhishek Poonia
|
||||
* Ayushi Negi
|
||||
* Harisanker Pradeep
|
||||
* Hassan Ismaeel
|
||||
* Jatin Rao
|
||||
* Meghna Gupta
|
||||
* S Balakrishnan
|
||||
* Suchit Kar
|
||||
* Tanvi Dadu
|
||||
* Ujjwal Agrawal
|
||||
* Mansi Agarwal
|
||||
* Siddharth Vaish
|
||||
* Ashish Kumar
|
||||
* Ilgaz Er
|
||||
* Alicia Bendz
|
||||
* Kaartic Sivaraam
|
||||
* Vanshika Arora
|
||||
* Seán Mac Gillicuddy
|
||||
|
||||
3rd party open source libraries used:
|
||||
* 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
|
||||
|
||||
===========================================================================
|
||||
|
||||
The Wikimedia Commons Android app uses portions of MapBox.
|
||||
|
||||
mapbox-gl-native copyright (c) 2014-2018 Mapbox.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Android Gesture Detectors Framework.
|
||||
|
||||
Copyright (c) 2012, Almer Thie
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Android Support Library.
|
||||
|
||||
Copyright (c) 2005-2013, The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Boost.
|
||||
|
||||
Distributed under the Boost Software License, Version 1.0.
|
||||
|
||||
http://www.boost.org/LICENSE_1_0.txt
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Clipper.
|
||||
|
||||
Author : Angus Johnson
|
||||
Version : 6.1.3a
|
||||
Date : 22 January 2014
|
||||
Website : http://www.angusj.com
|
||||
Copyright : Angus Johnson 2010-2014
|
||||
|
||||
License:
|
||||
Use, modification & distribution is subject to Boost Software License Ver 1.
|
||||
http://www.boost.org/LICENSE_1_0.txt
|
||||
|
||||
Attributions:
|
||||
The code in this library is an extension of Bala Vatti's clipping algorithm:
|
||||
"A generic solution to polygon clipping"
|
||||
Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63.
|
||||
http://portal.acm.org/citation.cfm?id=129906
|
||||
|
||||
Computer graphics and geometric modeling: implementation and algorithms
|
||||
By Max K. Agoston
|
||||
Springer; 1 edition (January 4, 2005)
|
||||
http://books.google.com/books?q=vatti+clipping+agoston
|
||||
|
||||
See also:
|
||||
"Polygon Offsetting by Computing Winding Numbers"
|
||||
Paper no. DETC2005-85513 pp. 565-575
|
||||
ASME 2005 International Design Engineering Technical Conferences
|
||||
and Computers and Information in Engineering Conference (IDETC/CIE2005)
|
||||
September 24-28, 2005 , Long Beach, California, USA
|
||||
http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of BugshotKit.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 marcoarment
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of CSS Color Parser.
|
||||
|
||||
(c) Dean McNamee <dean@gmail.com>, 2012.
|
||||
C++ port by Konstantin Käfer <mail@kkaefer.com>, 2014.
|
||||
|
||||
https://github.com/deanm/css-color-parser-js
|
||||
https://github.com/kkaefer/css-color-parser-cpp
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of GLFW.
|
||||
|
||||
Copyright (c) 2002-2006 Marcus Geelnard
|
||||
Copyright (c) 2006-2010 Camilla Berglund <elmindreda@elmindreda.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would
|
||||
be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not
|
||||
be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libc++.
|
||||
|
||||
The libc++ library is dual licensed under both the University of Illinois
|
||||
"BSD-Like" license and the MIT license. As a user of this code you may choose
|
||||
to use it under either license. As a contributor, you agree to allow your code
|
||||
to be used under both.
|
||||
|
||||
Full text of the relevant licenses is included below.
|
||||
|
||||
====
|
||||
|
||||
University of Illinois/NCSA
|
||||
Open Source License
|
||||
|
||||
Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Developed by:
|
||||
|
||||
LLVM Team
|
||||
|
||||
University of Illinois at Urbana-Champaign
|
||||
|
||||
http://llvm.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal with
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimers.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimers in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the names of the LLVM Team, University of Illinois at
|
||||
Urbana-Champaign, nor the names of its contributors may be used to
|
||||
endorse or promote products derived from this Software without specific
|
||||
prior written permission.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
|
||||
SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libcurl.
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright (c) 1996 - 2015, Daniel Stenberg, <daniel@haxx.se>.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright
|
||||
notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall not
|
||||
be used in advertising or otherwise to promote the sale, use or other dealings
|
||||
in this Software without prior written authorization of the copyright holder.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libjpeg-turbo.
|
||||
|
||||
This software is based in part on the work of the Independent JPEG Group.
|
||||
|
||||
Copyright (C)2009-2015 D. R. Commander. All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
- Neither the name of the libjpeg-turbo Project nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libpng.
|
||||
|
||||
This copy of the libpng notices is provided for your convenience. In case of
|
||||
any discrepancy between this copy and the notices in the file png.h that is
|
||||
included in the libpng distribution, the latter shall prevail.
|
||||
|
||||
COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:
|
||||
|
||||
If you modify libpng you may insert additional notices immediately following
|
||||
this sentence.
|
||||
|
||||
This code is released under the libpng license.
|
||||
|
||||
libpng versions 1.0.7, July 1, 2000, through 1.6.18, July 23, 2015, are
|
||||
Copyright (c) 2000-2002, 2004, 2006-2015 Glenn Randers-Pehrson, and are
|
||||
distributed according to the same disclaimer and license as libpng-1.0.6
|
||||
with the following individuals added to the list of Contributing Authors:
|
||||
|
||||
Simon-Pierre Cadieux
|
||||
Eric S. Raymond
|
||||
Mans Rullgard
|
||||
Cosmin Truta
|
||||
Gilles Vollant
|
||||
James Yu
|
||||
|
||||
and with the following additions to the disclaimer:
|
||||
|
||||
There is no warranty against interference with your enjoyment of the
|
||||
library or against infringement. There is no warranty that our
|
||||
efforts or the library will fulfill any of your particular purposes
|
||||
or needs. This library is provided with all faults, and the entire
|
||||
risk of satisfactory quality, performance, accuracy, and effort is with
|
||||
the user.
|
||||
|
||||
libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
|
||||
Copyright (c) 1998-2000 Glenn Randers-Pehrson, and are distributed according
|
||||
to the same disclaimer and license as libpng-0.96, with the following
|
||||
individuals added to the list of Contributing Authors:
|
||||
|
||||
Tom Lane
|
||||
Glenn Randers-Pehrson
|
||||
Willem van Schaik
|
||||
|
||||
libpng versions 0.89, June 1996, through 0.96, May 1997, are
|
||||
Copyright (c) 1996-1997 Andreas Dilger, and are
|
||||
distributed according to the same disclaimer and license as libpng-0.88,
|
||||
with the following individuals added to the list of Contributing Authors:
|
||||
|
||||
John Bowler
|
||||
Kevin Bracey
|
||||
Sam Bushell
|
||||
Magnus Holmgren
|
||||
Greg Roelofs
|
||||
Tom Tanner
|
||||
|
||||
libpng versions 0.5, May 1995, through 0.88, January 1996, are
|
||||
Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
|
||||
|
||||
For the purposes of this copyright and license, "Contributing Authors"
|
||||
is defined as the following set of individuals:
|
||||
|
||||
Andreas Dilger
|
||||
Dave Martindale
|
||||
Guy Eric Schalnat
|
||||
Paul Schmidt
|
||||
Tim Wegner
|
||||
|
||||
The PNG Reference Library is supplied "AS IS". The Contributing Authors
|
||||
and Group 42, Inc. disclaim all warranties, expressed or implied,
|
||||
including, without limitation, the warranties of merchantability and of
|
||||
fitness for any purpose. The Contributing Authors and Group 42, Inc.
|
||||
assume no liability for direct, indirect, incidental, special, exemplary,
|
||||
or consequential damages, which may result from the use of the PNG
|
||||
Reference Library, even if advised of the possibility of such damage.
|
||||
|
||||
Permission is hereby granted to use, copy, modify, and distribute this
|
||||
source code, or portions hereof, for any purpose, without fee, subject
|
||||
to the following restrictions:
|
||||
|
||||
1. The origin of this source code must not be misrepresented.
|
||||
|
||||
2. Altered versions must be plainly marked as such and must not
|
||||
be misrepresented as being the original source.
|
||||
|
||||
3. This Copyright notice may not be removed or altered from any
|
||||
source or altered source distribution.
|
||||
|
||||
The Contributing Authors and Group 42, Inc. specifically permit, without
|
||||
fee, and encourage the use of this source code as a component to
|
||||
supporting the PNG file format in commercial products. If you use this
|
||||
source code in a product, acknowledgment is not required but would be
|
||||
appreciated.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libuv.
|
||||
|
||||
libuv is part of the Node project: http://nodejs.org/
|
||||
libuv may be distributed alone under Node's license:
|
||||
|
||||
====
|
||||
|
||||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
This license applies to all parts of libuv that are not externally
|
||||
maintained libraries.
|
||||
|
||||
The externally maintained libraries used by libuv are:
|
||||
|
||||
- tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license.
|
||||
|
||||
- inet_pton and inet_ntop implementations, contained in src/inet.c, are
|
||||
copyright the Internet Systems Consortium, Inc., and licensed under the ISC
|
||||
license.
|
||||
|
||||
- stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three
|
||||
clause BSD license.
|
||||
|
||||
- pthread-fixes.h, pthread-fixes.c, copyright Google Inc. and Sony Mobile
|
||||
Communications AB. Three clause BSD license.
|
||||
|
||||
- android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design
|
||||
Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement
|
||||
n° 289016). Three clause BSD license.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of libzip.
|
||||
|
||||
Copyright (C) 1999-2014 Dieter Baron and Thomas Klausner
|
||||
|
||||
The authors can be contacted at <libzip@nih.at>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
3. The names of the authors may not be used to endorse or promote
|
||||
products derived from this software without specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of LOST.
|
||||
|
||||
Copyright (c) 2014 Mapzen
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the
|
||||
Route-Me open source project, including the Alpstein fork of it.
|
||||
|
||||
The Route-Me license appears below.
|
||||
|
||||
Copyright (c) 2008-2013, Route-Me Contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of nunicode.
|
||||
|
||||
Copyright (c) 2013 Aleksey Tulinov <aleksey.tulinov@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of OkHTTP.
|
||||
|
||||
Copyright 2014 Square, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of OpenSSL.
|
||||
|
||||
LICENSE ISSUES
|
||||
==============
|
||||
|
||||
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
|
||||
the OpenSSL License and the original SSLeay license apply to the toolkit.
|
||||
See below for the actual license texts. Actually both licenses are BSD-style
|
||||
Open Source licenses. In case of any license issues related to OpenSSL
|
||||
please contact openssl-core@openssl.org.
|
||||
|
||||
OpenSSL License
|
||||
---------------
|
||||
|
||||
Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this
|
||||
software must display the following acknowledgment:
|
||||
"This product includes software developed by the OpenSSL Project
|
||||
for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||
|
||||
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
|
||||
endorse or promote products derived from this software without
|
||||
prior written permission. For written permission, please contact
|
||||
openssl-core@openssl.org.
|
||||
|
||||
5. Products derived from this software may not be called "OpenSSL"
|
||||
nor may "OpenSSL" appear in their names without prior written
|
||||
permission of the OpenSSL Project.
|
||||
|
||||
6. Redistributions of any form whatsoever must retain the following
|
||||
acknowledgment:
|
||||
"This product includes software developed by the OpenSSL Project
|
||||
for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
|
||||
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
|
||||
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
This product includes cryptographic software written by Eric Young
|
||||
(eay@cryptsoft.com). This product includes software written by Tim
|
||||
Hudson (tjh@cryptsoft.com).
|
||||
|
||||
Original SSLeay License
|
||||
-----------------------
|
||||
|
||||
Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
All rights reserved.
|
||||
|
||||
This package is an SSL implementation written
|
||||
by Eric Young (eay@cryptsoft.com).
|
||||
The implementation was written so as to conform with Netscapes SSL.
|
||||
|
||||
This library is free for commercial and non-commercial use as long as
|
||||
The following conditions are aheared to. The following conditions
|
||||
apply to all code found in this distribution, be it the RC4, RSA,
|
||||
lhash, DES, etc., code; not just the SSL code. The SSL documentation
|
||||
included with this distribution is covered by the same copyright terms
|
||||
except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||
|
||||
Copyright remains Eric Young's, and as such any Copyright notices in
|
||||
the code are not to be removed.
|
||||
If this package is used in a product, Eric Young should be given attribution
|
||||
as the author of the parts of the library used.
|
||||
This can be in the form of a textual message at program startup or
|
||||
in documentation (online or textual) provided with the package.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
"This product includes cryptographic software written by
|
||||
Eric Young (eay@cryptsoft.com)"
|
||||
The word 'cryptographic' can be left out if the rouines from the library
|
||||
being used are not cryptographic related :-).
|
||||
4. If you include any Windows specific code (or a derivative thereof) from
|
||||
the apps directory (application code) you must include an acknowledgement:
|
||||
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
The licence and distribution terms for any publically available version or
|
||||
derivative of this code cannot be changed. i.e. this code cannot simply be
|
||||
copied and put under another distribution licence
|
||||
[including the GNU Public Licence.]
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of RapidJSON.
|
||||
|
||||
Tencent is pleased to support the open source community by making RapidJSON
|
||||
available.
|
||||
|
||||
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights
|
||||
reserved.
|
||||
|
||||
If you have downloaded a copy of the RapidJSON binary from Tencent, please note
|
||||
that the RapidJSON binary is licensed under the MIT License. If you have
|
||||
downloaded a copy of the RapidJSON source code from Tencent, please note that
|
||||
RapidJSON source code is licensed under the MIT License, except for the third-
|
||||
party components listed below which are subject to different license terms.
|
||||
Your integration of RapidJSON into your own projects may require compliance with
|
||||
the MIT License, as well as the other licenses applicable to the third-party
|
||||
components included within RapidJSON. To avoid the problematic JSON license in
|
||||
your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as
|
||||
it's the only code under the JSON license. A copy of the MIT License is included
|
||||
in this file.
|
||||
|
||||
Other dependencies and licenses:
|
||||
|
||||
Open Source Software Licensed Under the BSD License:
|
||||
--------------------------------------------------------------------
|
||||
|
||||
The msinttypes r29
|
||||
Copyright (c) 2006-2013 Alexander Chemeris
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of copyright holder nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Open Source Software Licensed Under the JSON License:
|
||||
--------------------------------------------------------------------
|
||||
|
||||
json.org
|
||||
Copyright (c) 2002 JSON.org
|
||||
All Rights Reserved.
|
||||
|
||||
JSON_checker
|
||||
Copyright (c) 2002 JSON.org
|
||||
All Rights Reserved.
|
||||
|
||||
Terms of the JSON License:
|
||||
---------------------------------------------------
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
The Software shall be used for Good, not Evil.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Terms of the MIT License:
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Reachability.
|
||||
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of SQLite.
|
||||
|
||||
2001 September 15
|
||||
|
||||
The author disclaims copyright to this source code. In place of
|
||||
a legal notice, here is a blessing:
|
||||
|
||||
May you do good and not evil.
|
||||
May you find forgiveness for yourself and forgive others.
|
||||
May you share freely, never taking more than you give.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of SVPulsingAnnotationView.
|
||||
|
||||
Copyright (c) 2013, Sam Vermette <hello@samvermette.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of zlib.
|
||||
|
||||
Acknowledgments:
|
||||
|
||||
The deflate format used by zlib was defined by Phil Katz. The deflate and
|
||||
zlib specifications were written by L. Peter Deutsch. Thanks to all the
|
||||
people who reported problems and suggested various improvements in zlib; they
|
||||
are too numerous to cite here.
|
||||
|
||||
Copyright notice:
|
||||
|
||||
(C) 1995-2013 Jean-loup Gailly and Mark Adler
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Jean-loup Gailly Mark Adler
|
||||
jloup@gzip.org madler@alumni.caltech.edu
|
||||
|
||||
===========================================================================
|
||||
|
||||
Mapbox GL uses portions of Realm Objective-C.
|
||||
|
||||
Copyright 2015 Realm Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
|
|
|||
56
README.md
|
|
@ -1,58 +1,36 @@
|
|||
# Wikimedia Commons Android app
|
||||

|
||||
[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain)
|
||||
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
|
||||
[](https://codecov.io/gh/commons-app/apps-android-commons)
|
||||
# Wikimedia Commons Android app [](https://travis-ci.org/commons-app/apps-android-commons)
|
||||
|
||||
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>
|
||||
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=fr.free.nrw.commons" target="_blank">
|
||||
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="90"/></a>
|
||||
|
||||
## Documentation
|
||||
|
||||
Our [documentation repository][4] contains extensive documentation for users, contributors, and developers alike:
|
||||
We try to have an extensive documentation at [our wiki here at Github][5]:
|
||||
|
||||
* [User Documentation][5]
|
||||
* [Contributor Documentation][6]
|
||||
* [Volunteers Welcome!][7]
|
||||
* [User Documentation][6]
|
||||
* [Contributor Documentation][7]
|
||||
* [Volunteers Welcome!][9]
|
||||
* [Developer Documentation][8]
|
||||
* [Libraries Used][9]
|
||||
|
||||
## Contributors ##
|
||||
|
||||
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/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<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/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/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<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/346271?v=4" width="100px;"/><br /><sub><b>amire80</b></sub>](https://github.com/amire80) | [<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/83745993?v=4" width="100px;"/><br /><sub><b>RitikaPahwa4444</b></sub>](https://github.com/RitikaPahwa4444) | [<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/101377978?v=4" width="100px;"/><br /><sub><b>rohit9625</b></sub>](https://github.com/rohit9625) | [<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/111801812?v=4" width="100px;"/><br /><sub><b>parneet-guraya</b></sub>](https://github.com/parneet-guraya) | [<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) |
|
||||
|
||||
|
||||
|
||||
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
||||
|
||||
## License ##
|
||||
|
||||
This software is open source, licensed under the [Apache License 2.0][10].
|
||||
This software is open source, licensed under the [Apache License 2.0][4].
|
||||
|
||||
|
||||
|
||||
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||
[2]: https://commons-app.github.io/
|
||||
[3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream
|
||||
|
||||
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
||||
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
||||
[6]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#️-contributor-documentation
|
||||
[7]: https://github.com/commons-app/commons-app-documentation/blob/master/android/Volunteers-welcome!.md#volunteers-welcome
|
||||
[8]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-developer-documentation
|
||||
[9]: https://github.com/commons-app/commons-app-documentation/blob/master/android/Libraries-used.md#libraries-used
|
||||
|
||||
[10]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
||||
[4]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[5]: https://github.com/commons-app/apps-android-commons/wiki
|
||||
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
|
||||
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
|
||||
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
|
||||
[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
|
||||
|
|
|
|||
160
app/build.gradle
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
apply from: '../gitutils.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco-android'
|
||||
apply from: 'quality.gradle'
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
|
||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
||||
implementation 'in.yuvi:http.fluent:1.3'
|
||||
implementation 'com.android.volley:volley:1.0.0'
|
||||
implementation 'ch.acra:acra:4.7.0'
|
||||
implementation 'org.mediawiki:api:1.3'
|
||||
implementation 'commons-codec:commons-codec:1.10'
|
||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.5.1'
|
||||
implementation 'info.debatty:java-string-similarity:0.24'
|
||||
implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){
|
||||
transitive=true
|
||||
}
|
||||
|
||||
|
||||
implementation "com.android.support:support-v4:${project.supportLibVersion}"
|
||||
implementation "com.android.support:appcompat-v7:${project.supportLibVersion}"
|
||||
implementation "com.android.support:design:${project.supportLibVersion}"
|
||||
|
||||
implementation "com.android.support:cardview-v7:${project.supportLibVersion}"
|
||||
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
|
||||
implementation 'com.squareup.okio:okio:1.13.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||
|
||||
implementation 'com.facebook.fresco:fresco:1.3.0'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
||||
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||
|
||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.robolectric:robolectric:3.4'
|
||||
testImplementation 'org.mockito:mockito-all:1.10.19'
|
||||
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
|
||||
androidTestImplementation "com.android.support:support-annotations:${project.supportLibVersion}"
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
|
||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
||||
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
|
||||
|
||||
implementation 'com.google.dagger:dagger:2.11'
|
||||
implementation 'com.google.dagger:dagger-android-support:2.11'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.11'
|
||||
annotationProcessor 'com.google.dagger:dagger-android-processor:2.11'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion project.compileSdkVersion
|
||||
buildToolsVersion project.buildToolsVersion
|
||||
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'fr.free.nrw.commons'
|
||||
versionCode 80
|
||||
versionName '2.6.5'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion project.minSdkVersion
|
||||
targetSdkVersion project.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
testCoverageEnabled true
|
||||
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'tier'
|
||||
productFlavors {
|
||||
prod {
|
||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
|
||||
buildConfigField "String", "EVENTLOG_URL", "\"https://www.wikimedia.org/beacon/event\""
|
||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
dimension 'tier'
|
||||
}
|
||||
|
||||
beta {
|
||||
// What values do we need to hit the BETA versions of the site / api ?
|
||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
|
||||
buildConfigField "String", "EVENTLOG_URL", "\"https://commons.wikimedia.beta.wmflabs.org/beacon/event\""
|
||||
buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\""
|
||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||
dimension 'tier'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
disable 'ExtraTranslation'
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
|
||||
configurations.all {
|
||||
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
|
||||
exclude module: 'httpclient'
|
||||
exclude module: 'commons-logging'
|
||||
}
|
||||
buildToolsVersion buildToolsVersion
|
||||
}
|
||||
|
|
@ -1,447 +0,0 @@
|
|||
import java.util.Properties
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.kapt)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/jacoco.gradle")
|
||||
|
||||
val isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file("../play.p12").exists()
|
||||
|
||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||
apply(plugin = "com.github.triplet.play")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "fr.free.nrw.commons"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "fr.free.nrw.commons"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 1059
|
||||
versionName = "6.1.0"
|
||||
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("test") {
|
||||
// Use kotlin only in tests (for now)
|
||||
java.srcDirs("src/test/kotlin")
|
||||
|
||||
// Use main assets and resources in test
|
||||
assets.srcDirs("src/main/assets")
|
||||
resources.srcDirs("src/main/resources")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
// Configure keystore based on env vars in Travis for automated alpha builds
|
||||
if(isRunningOnTravisAndIsNotPRBuild) {
|
||||
storeFile = file("../nr-commons.keystore")
|
||||
storePassword = System.getenv("keystore_password")
|
||||
keyAlias = System.getenv("key_alias")
|
||||
keyPassword = System.getenv("key_password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||
testProguardFile("test-proguard-rules.txt")
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||
testProguardFile("test-proguard-rules.txt")
|
||||
|
||||
versionNameSuffix = "-debug-" + getBranchName()
|
||||
enableUnitTestCoverage = true
|
||||
enableAndroidTestCoverage = true
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force("androidx.annotation:annotation:1.1.0")
|
||||
force("com.jakewharton.timber:timber:4.7.1")
|
||||
force("androidx.fragment:fragment:1.3.6")
|
||||
}
|
||||
exclude(module = "okhttp-ws")
|
||||
}
|
||||
|
||||
flavorDimensions += "tier"
|
||||
productFlavors {
|
||||
create("prod") {
|
||||
dimension = "tier"
|
||||
applicationId = "fr.free.nrw.commons"
|
||||
|
||||
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
|
||||
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"")
|
||||
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
|
||||
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
|
||||
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"")
|
||||
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"")
|
||||
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"")
|
||||
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.org\"")
|
||||
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"")
|
||||
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"")
|
||||
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
|
||||
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", "\"" + getTestUserName() + "\"")
|
||||
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
|
||||
buildConfigField("String", "DEPICTS_PROPERTY", "\"P180\"")
|
||||
buildConfigField("String", "CREATOR_PROPERTY", "\"P170\"")
|
||||
}
|
||||
|
||||
create("beta") {
|
||||
dimension = "tier"
|
||||
applicationId = "fr.free.nrw.commons.beta"
|
||||
|
||||
// What values do we need to hit the BETA versions of the site / api ?
|
||||
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
|
||||
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"")
|
||||
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
|
||||
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
|
||||
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"")
|
||||
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"")
|
||||
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"")
|
||||
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
|
||||
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", "\"" + getTestUserName() + "\"")
|
||||
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
|
||||
buildConfigField("String", "DEPICTS_PROPERTY", "\"P245962\"")
|
||||
buildConfigField("String", "CREATOR_PROPERTY", "\"P253075\"")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
buildToolsVersion = buildToolsVersion
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.8"
|
||||
}
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes += listOf("META-INF/androidx.*")
|
||||
}
|
||||
resources {
|
||||
excludes += listOf(
|
||||
"META-INF/androidx.*",
|
||||
"META-INF/proguard/androidx-annotations.pro",
|
||||
"/META-INF/LICENSE.md",
|
||||
"/META-INF/LICENSE-notice.md"
|
||||
)
|
||||
}
|
||||
}
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
unitTests {
|
||||
isReturnDefaultValues = true
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
unitTests.all {
|
||||
it.jvmArgs("-noverify")
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError = false
|
||||
disable += listOf("MissingTranslation", "ExtraTranslation")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Utils
|
||||
implementation(libs.gson)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.retrofit.adapter.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxbinding)
|
||||
implementation(libs.rxbinding.appcompat)
|
||||
implementation(libs.facebook.fresco)
|
||||
implementation(libs.facebook.fresco.middleware)
|
||||
implementation(libs.apache.commons.lang3)
|
||||
|
||||
// UI
|
||||
implementation("${libs.viewpagerindicator.library.get()}@aar")
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.android.sdk)
|
||||
implementation(libs.android.plugin.scalebar)
|
||||
|
||||
implementation(libs.timber)
|
||||
implementation(libs.android.material)
|
||||
implementation(libs.dexter)
|
||||
|
||||
// Jetpack Compose
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.ui.viewbinding)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.foundation)
|
||||
implementation(libs.androidx.foundation.layout)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation(libs.adapterdelegates4.kotlin.dsl.viewbinding)
|
||||
implementation(libs.adapterdelegates4.pagination)
|
||||
implementation(libs.androidx.paging.runtime.ktx)
|
||||
testImplementation(libs.androidx.paging.common.ktx)
|
||||
implementation(libs.androidx.paging.rxjava2.ktx)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
|
||||
// Logging
|
||||
implementation(libs.acra.dialog)
|
||||
implementation(libs.acra.mail)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.logback.android.classic) {
|
||||
exclude(group = "com.google.android", module = "android")
|
||||
}
|
||||
implementation(libs.logging.interceptor)
|
||||
|
||||
// Dependency injector
|
||||
implementation(libs.dagger.android)
|
||||
implementation(libs.dagger.android.support)
|
||||
kapt(libs.dagger.android.processor)
|
||||
kapt(libs.dagger.compiler)
|
||||
annotationProcessor(libs.dagger.android.processor)
|
||||
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
//Mocking
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.powermock.module.junit)
|
||||
testImplementation(libs.powermock.api.mockito)
|
||||
testImplementation(libs.mockk)
|
||||
|
||||
// Unit testing
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.androidx.runner)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.androidx.test.rules)
|
||||
testImplementation(libs.mockwebserver)
|
||||
testImplementation(libs.livedata.testing.ktx)
|
||||
testImplementation(libs.androidx.core.testing)
|
||||
testImplementation(libs.junit.jupiter.api)
|
||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||
testImplementation(libs.soloader)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
debugImplementation(libs.androidx.fragment.testing)
|
||||
testImplementation(libs.commons.io)
|
||||
|
||||
// Android testing
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.espresso.intents)
|
||||
androidTestImplementation(libs.androidx.espresso.contrib)
|
||||
androidTestImplementation(libs.androidx.runner)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.core)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.androidx.annotation)
|
||||
androidTestImplementation(libs.mockwebserver)
|
||||
androidTestImplementation(libs.androidx.uiautomator)
|
||||
|
||||
// Debugging
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
|
||||
// Support libraries
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.cardview)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.recyclerview.fastscroll)
|
||||
|
||||
//swipe_layout
|
||||
implementation(libs.swipelayout.library)
|
||||
|
||||
//Room
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.room.rxjava)
|
||||
kapt(libs.androidx.room.compiler)
|
||||
|
||||
// Preferences
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.androidx.preference.ktx)
|
||||
|
||||
//Android Media
|
||||
implementation(libs.juanitobananas.androidDmediaUtil)
|
||||
implementation(libs.androidx.multidex)
|
||||
|
||||
// Kotlin + coroutines
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.kotlinx.coroutines.rx2)
|
||||
testImplementation(libs.androidx.work.testing)
|
||||
|
||||
//Glide
|
||||
implementation(libs.glide)
|
||||
annotationProcessor(libs.glide.compiler)
|
||||
kaptTest(libs.androidx.databinding.compiler)
|
||||
kaptAndroidTest(libs.androidx.databinding.compiler)
|
||||
|
||||
implementation(libs.coordinates2country.android) {
|
||||
exclude(group = "com.google.android", module = "android")
|
||||
}
|
||||
|
||||
//OSMDroid
|
||||
implementation(libs.osmdroid.android)
|
||||
constraints {
|
||||
implementation(libs.kotlin.stdlib.jdk7) {
|
||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||
}
|
||||
implementation(libs.kotlin.stdlib.jdk8) {
|
||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Exec>("disableAnimations") {
|
||||
val adb = "${System.getenv("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 {
|
||||
val connectedBetaDebugAndroidTest = tasks.named("connectedBetaDebugAndroidTest")
|
||||
val connectedProdDebugAndroidTest = tasks.named("connectedProdDebugAndroidTest")
|
||||
|
||||
connectedBetaDebugAndroidTest.configure {
|
||||
dependsOn("disableAnimations")
|
||||
}
|
||||
connectedProdDebugAndroidTest.configure {
|
||||
dependsOn("disableAnimations")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTestUserName(): String? {
|
||||
val propFile = rootProject.file("./local.properties")
|
||||
val properties = Properties()
|
||||
propFile.inputStream().use { properties.load(it) }
|
||||
return properties.getProperty("TEST_USER_NAME")
|
||||
}
|
||||
|
||||
fun getTestPassword(): String? {
|
||||
val propFile = rootProject.file("./local.properties")
|
||||
val properties = Properties()
|
||||
propFile.inputStream().use { properties.load(it) }
|
||||
return properties.getProperty("TEST_USER_PASSWORD")
|
||||
}
|
||||
|
||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||
configure<com.github.triplet.gradle.play.PlayPublisherExtension> {
|
||||
track = "alpha"
|
||||
userFraction = 1.0
|
||||
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
|
||||
serviceAccountCredentials = file("../play.p12")
|
||||
|
||||
resolutionStrategy = "auto"
|
||||
outputProcessor { // this: ApkVariantOutput
|
||||
versionNameOverride = "$versionNameOverride.$versionCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuildVersion(): String? {
|
||||
return try {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine("git", "rev-parse", "--short", "HEAD")
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString().trim()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getBranchName(): String? {
|
||||
return try {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString().trim()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +1,5 @@
|
|||
-dontobfuscate
|
||||
-ignorewarnings
|
||||
|
||||
-dontnote **
|
||||
-dontwarn net.bytebuddy.**
|
||||
-dontwarn org.mockito.**
|
||||
|
||||
# --- Apache ---
|
||||
-keep class org.apache.http.** { *; }
|
||||
-dontwarn org.apache.**
|
||||
# --- /Apache ---
|
||||
|
||||
# --- Butter Knife ---
|
||||
# Finder.castParam() is stripped when not needed and ProGuard notes it
|
||||
# unnecessarily. When castParam() is needed, it's not stripped. e.g.:
|
||||
#
|
||||
# @OnItemSelected(value = R.id.history_entry_list)
|
||||
# void foo(ListView bar) {
|
||||
# L.d("baz");
|
||||
# }
|
||||
|
||||
-dontnote butterknife.internal.**
|
||||
# --- /Butter Knife ---
|
||||
|
||||
# --- Retrofit2 ---
|
||||
# Platform calls Class.forName on types which do not exist on Android to determine platform.
|
||||
-dontnote retrofit2.Platform
|
||||
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-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 ---
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
# --- /OkHttp + Okio ---
|
||||
|
||||
# --- Gson ---
|
||||
# https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg
|
||||
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { *; }
|
||||
|
||||
# Prevent R8 from obfuscating project classes used by Gson for parsing
|
||||
-keep class fr.free.nrw.commons.fileusages.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
# --- /Gson ---
|
||||
|
||||
|
||||
# --- /logback ---
|
||||
|
||||
-keep class ch.qos.** { *; }
|
||||
-keep class org.slf4j.** { *; }
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-dontwarn ch.qos.logback.core.net.*
|
||||
|
||||
# --- /acra ---
|
||||
-keep class org.acra.** { *; }
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# --- /recycler view ---
|
||||
-keep class androidx.recyclerview.widget.RecyclerView {
|
||||
public androidx.recyclerview.widget.RecyclerView$ViewHolder findViewHolderForPosition(int);
|
||||
}
|
||||
# --- Parcelable ---
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
static ** CREATOR;
|
||||
}
|
||||
-dontwarn org.apache.http.**
|
||||
-keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;}
|
||||
-keep class android.support.v7.widget.ShareActionProvider { *; }
|
||||
45
app/quality.gradle
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'pmd'
|
||||
|
||||
check.dependsOn 'checkstyle', 'pmd'
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '7.5.1'
|
||||
}
|
||||
|
||||
task checkstyle(type: Checkstyle) {
|
||||
configFile file("${project.rootDir}/script/style/checkstyle.xml")
|
||||
source 'src'
|
||||
include '**/*.java'
|
||||
exclude '**/gen/**'
|
||||
|
||||
classpath = files()
|
||||
|
||||
reports {
|
||||
html {
|
||||
enabled true
|
||||
destination "${project.buildDir}/reports/checkstyle/checkstyle.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task pmd(type: Pmd) {
|
||||
ignoreFailures = true
|
||||
ruleSetFiles = files("${project.rootDir}/script/style/ruleset.xml")
|
||||
ruleSets = []
|
||||
|
||||
source 'src'
|
||||
include '**/*.java'
|
||||
exclude '**/gen/**'
|
||||
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination "${project.buildDir}/reports/pmd/pmd.xml"
|
||||
}
|
||||
html {
|
||||
destination "${project.buildDir}/reports/pmd/pmd.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
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.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
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
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))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuildNumber() {
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchFacebook() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.facebook_launch_icon)).perform(ViewActions.click())
|
||||
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 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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@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.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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.Espresso
|
||||
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.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.uiautomator.UiDevice
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
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
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LoginActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
Intents.init()
|
||||
UITestHelper.skipWelcome()
|
||||
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForgotPassword() {
|
||||
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,214 +0,0 @@
|
|||
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.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<*> = 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 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
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,59 +0,0 @@
|
|||
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.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
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SearchActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.test.espresso.Espresso;
|
||||
import android.support.test.espresso.action.ViewActions;
|
||||
import android.support.test.espresso.assertion.ViewAssertions;
|
||||
import android.support.test.espresso.matcher.ViewMatchers;
|
||||
import android.support.test.filters.LargeTest;
|
||||
import android.support.test.rule.ActivityTestRule;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SettingsActivityTest {
|
||||
private SharedPreferences prefs;
|
||||
private Map<String,?> prefValues;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<SettingsActivity> activityRule =
|
||||
new ActivityTestRule<SettingsActivity>(SettingsActivity.class,
|
||||
false /* Initial touch mode */, true /* launch activity */) {
|
||||
|
||||
@Override
|
||||
protected void afterActivityLaunched() {
|
||||
// save preferences
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this.getActivity());
|
||||
prefValues = prefs.getAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterActivityFinished() {
|
||||
// restore preferences
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
for (Map.Entry<String,?> entry: prefValues.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object val = entry.getValue();
|
||||
if (val instanceof String) {
|
||||
editor.putString(key, (String)val);
|
||||
} else if (val instanceof Boolean) {
|
||||
editor.putBoolean(key, (Boolean)val);
|
||||
} else if (val instanceof Integer) {
|
||||
editor.putInt(key, (Integer)val);
|
||||
} else {
|
||||
throw new RuntimeException("type not implemented: " + entry);
|
||||
}
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void oneLicenseIsChecked() {
|
||||
// click "License" (the first item)
|
||||
Espresso.onData(Matchers.anything())
|
||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||
.atPosition(0)
|
||||
.perform(ViewActions.click());
|
||||
|
||||
// test the selected item
|
||||
Espresso.onView(ViewMatchers.isChecked())
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void afterClickingCcby4ItWillStay() {
|
||||
// click "License" (the first item)
|
||||
Espresso.onData(Matchers.anything())
|
||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||
.atPosition(0)
|
||||
.perform(ViewActions.click());
|
||||
|
||||
// click "Attribution 4.0"
|
||||
Espresso.onView(
|
||||
ViewMatchers.withText(R.string.license_name_cc_by_four)
|
||||
).perform(ViewActions.click());
|
||||
|
||||
// click "License" (the first item)
|
||||
Espresso.onData(Matchers.anything())
|
||||
.inAdapterView(ViewMatchers.withId(android.R.id.list))
|
||||
.atPosition(0)
|
||||
.perform(ViewActions.click());
|
||||
|
||||
// test the value remains "Attribution 4.0"
|
||||
Espresso.onView(ViewMatchers.isChecked())
|
||||
.check(ViewAssertions.matches(
|
||||
ViewMatchers.withText(R.string.license_name_cc_by_four)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
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.assertion.ViewAssertions.matches
|
||||
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.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.SettingsActivity
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SettingsActivityTest {
|
||||
private lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@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 useAuthorNameTogglesOn() {
|
||||
// Turn on "Use author name" preference if currently off
|
||||
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||
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
|
||||
.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0),
|
||||
),
|
||||
).check(matches(isEnabled()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
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
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
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 {
|
||||
onView(ViewMatchers.withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
// Skip tutorial
|
||||
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
||||
.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
|
||||
sleep(3000)
|
||||
onView(ViewMatchers.withId(R.id.login_username))
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUsername()),
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_password))
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUserPassword()),
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_button))
|
||||
.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) {
|
||||
try {
|
||||
Timber.d("Sleeping for %d", timeInMillis)
|
||||
Thread.sleep(timeInMillis)
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTestUsername(): String {
|
||||
val username = BuildConfig.TEST_USERNAME
|
||||
if (StringUtils.isEmpty(username) || username == "null") {
|
||||
throw NotImplementedError("Configure your beta account's 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
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
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("should return first matching item")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UploadActivityTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(UploadActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
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 (_: 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 (_: 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
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.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
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.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 fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||
import fr.free.nrw.commons.util.MyViewAction
|
||||
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
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
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,
|
||||
)!!
|
||||
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
private val randomBitmap: Bitmap
|
||||
get() {
|
||||
val random = Random()
|
||||
val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
|
||||
bitmap.eraseColor(random.nextInt(255))
|
||||
return bitmap
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
try {
|
||||
Intents.init()
|
||||
} catch (_: IllegalStateException) {
|
||||
}
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Fix Failing Test")
|
||||
fun testUploadWithDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
}
|
||||
|
||||
setupSingleUpload("image.jpg")
|
||||
|
||||
openGallery()
|
||||
|
||||
// Validate that an intent to get an image is sent
|
||||
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
|
||||
|
||||
// Create filename with the current time (to prevent overwrites)
|
||||
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
|
||||
val commonsFileName = "MobileTest " + dateFormat.format(Date())
|
||||
|
||||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
||||
private fun dismissWarning(warningText: String) {
|
||||
try {
|
||||
onView(withText(warningText))
|
||||
.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!")
|
||||
}
|
||||
|
||||
setupSingleUpload("image.jpg")
|
||||
|
||||
openGallery()
|
||||
|
||||
// Validate that an intent to get an image is sent
|
||||
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
|
||||
|
||||
// Create filename with the current time (to prevent overwrites)
|
||||
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
|
||||
val commonsFileName = "MobileTest " + dateFormat.format(Date())
|
||||
|
||||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
dismissWarning("Yes")
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
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!")
|
||||
}
|
||||
|
||||
setupSingleUpload("image.jpg")
|
||||
|
||||
openGallery()
|
||||
|
||||
// Validate that an intent to get an image is sent
|
||||
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
|
||||
|
||||
// Create filename with the current time (to prevent overwrites)
|
||||
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
|
||||
val commonsFileName = "MobileTest " + dateFormat.format(Date())
|
||||
|
||||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarningDialog()
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.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"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(withId(R.id.btn_add))
|
||||
.perform(click())
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||
1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
||||
private fun setupSingleUpload(imageName: String) {
|
||||
saveToInternalStorage(imageName)
|
||||
singleImageIntent(imageName)
|
||||
}
|
||||
|
||||
private fun saveToInternalStorage(imageName: String) {
|
||||
val bitmapImage = randomBitmap
|
||||
|
||||
// path to /data/data/yourapp/app_data/imageDir
|
||||
val mypath = File(Environment.getExternalStorageDirectory(), imageName)
|
||||
|
||||
Timber.d("Filepath: %s", mypath.path)
|
||||
|
||||
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
|
||||
|
||||
var fos: FileOutputStream? = null
|
||||
try {
|
||||
fos = FileOutputStream(mypath)
|
||||
// Use the compress method on the BitMap object to write image to the OutputStream
|
||||
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
fos?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun singleImageIntent(imageName: String) {
|
||||
// Uri to return by our mock gallery selector
|
||||
// Requires file 'image.jpg' to be placed at root of file structure
|
||||
val imageUri = Uri.parse("file://mnt/sdcard/$imageName")
|
||||
|
||||
// Build a result to return from the Camera app
|
||||
val intent = Intent()
|
||||
intent.data = imageUri
|
||||
val result = ActivityResult(Activity.RESULT_OK, intent)
|
||||
|
||||
// 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
|
||||
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
|
||||
}
|
||||
|
||||
private fun dismissWarningDialog() {
|
||||
try {
|
||||
onView(withText("Yes"))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openGallery() {
|
||||
// Open FAB
|
||||
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
||||
.perform(click())
|
||||
|
||||
// Click gallery
|
||||
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
||||
.perform(click())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
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.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)
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifProdHidesSkipButton() {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.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())
|
||||
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())
|
||||
assertThat(true, equalTo(true))
|
||||
onView(withId(R.id.welcomePager))
|
||||
.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())
|
||||
assertThat(true, equalTo(true))
|
||||
onView(withId(R.id.welcomePager))
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
.perform(ViewActions.swipeRight())
|
||||
assertThat(true, equalTo(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swipeBeyondBounds() {
|
||||
val viewPager = activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
|
||||
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() {
|
||||
val viewPager = activityRule.activity.findViewById<ViewPager>(R.id.welcomePager)
|
||||
|
||||
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,271 +0,0 @@
|
|||
package fr.free.nrw.commons.contributions
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.OkHttpConnectionFactory
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.TestCommonsApplication
|
||||
import fr.free.nrw.commons.createTestClient
|
||||
import fr.free.nrw.commons.upload.WikidataPlace
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.robolectric.Shadows
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.LooperMode
|
||||
import java.lang.reflect.Method
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
class ContributionsListFragmentUnitTests {
|
||||
private lateinit var scenario: FragmentScenario<ContributionsListFragment>
|
||||
private lateinit var fragment: ContributionsListFragment
|
||||
|
||||
private val adapter: ContributionsListAdapter = mock()
|
||||
private val contribution: Contribution = mock()
|
||||
private val media: Media = mock()
|
||||
private val wikidataPlace: WikidataPlace = mock()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
OkHttpConnectionFactory.CLIENT = createTestClient()
|
||||
|
||||
scenario =
|
||||
launchFragmentInContainer(
|
||||
initialState = Lifecycle.State.RESUMED,
|
||||
themeResId = R.style.LightAppTheme,
|
||||
) {
|
||||
ContributionsListFragment()
|
||||
.apply {
|
||||
contributionsListPresenter = mock()
|
||||
callback = mock()
|
||||
}.also {
|
||||
fragment = it
|
||||
}
|
||||
}
|
||||
|
||||
scenario.onFragment {
|
||||
it.adapter = adapter
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun checkFragmentNotNull() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
Assert.assertNotNull(fragment)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnDetach() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.onDetach()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetContributionStateAt() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution)
|
||||
fragment.getContributionStateAt(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnScrollToTop() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.rvContributionsList = mock()
|
||||
fragment.scrollToTop()
|
||||
verify(fragment.rvContributionsList)?.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnConfirmClicked() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
`when`(contribution.media).thenReturn(media)
|
||||
`when`(media.wikiCode).thenReturn("")
|
||||
`when`(contribution.wikidataPlace).thenReturn(wikidataPlace)
|
||||
fragment.onConfirmClicked(contribution, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.totalMediaCount
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetMediaAtPositionCaseNonNull() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution)
|
||||
`when`(contribution.media).thenReturn(media)
|
||||
fragment.getMediaAtPosition(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetMediaAtPositionCaseNull() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(null)
|
||||
fragment.getMediaAtPosition(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testShowAddImageToWikipediaInstructions() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"showAddImageToWikipediaInstructions",
|
||||
Contribution::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment, contribution)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testAddImageToWikipedia() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.addImageToWikipedia(contribution)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOpenMediaDetail() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.openMediaDetail(0, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnViewStateRestored() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.onViewStateRestored(mock())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnSaveInstanceState() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.onSaveInstanceState(mock())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testShowNoContributionsUI() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.showNoContributionsUI(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testShowProgress() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.showProgress(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testShowWelcomeTip() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.showWelcomeTip(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testAnimateFAB() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment {
|
||||
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).hide()
|
||||
}
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"animateFAB",
|
||||
Boolean::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testAnimateFABCaseShownAndOpen() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment {
|
||||
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
|
||||
}
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"animateFAB",
|
||||
Boolean::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testAnimateFABCaseShownAndClose() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment {
|
||||
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
|
||||
}
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"animateFAB",
|
||||
Boolean::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testSetListeners() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"setListeners",
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testInitializeAnimations() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
val method: Method =
|
||||
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||
"initializeAnimations",
|
||||
)
|
||||
method.isAccessible = true
|
||||
method.invoke(fragment)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnConfigurationChanged() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
val newConfig: Configuration = mock()
|
||||
newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE
|
||||
fragment.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package fr.free.nrw.commons.navtab
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.TestCommonsApplication
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Shadows
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.LooperMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
class MoreBottomSheetLoggedOutFragmentUnitTests {
|
||||
private lateinit var scenario: FragmentScenario<MoreBottomSheetLoggedOutFragment>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
scenario =
|
||||
launchFragmentInContainer(
|
||||
initialState = Lifecycle.State.RESUMED,
|
||||
themeResId = R.style.LightAppTheme,
|
||||
) {
|
||||
MoreBottomSheetLoggedOutFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnSettingsClicked() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment { it.onSettingsClicked() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnAboutClicked() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment { it.onAboutClicked() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnFeedbackClicked() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment { it.onFeedbackClicked() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testOnLogoutClicked() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
scenario.onFragment { it.onLogoutClicked() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package fr.free.nrw.commons.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PasteSensitiveTextInputEditTextTest {
|
||||
private var context: Context? = null
|
||||
private var textView: PasteSensitiveTextInputEditText? = null
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
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,
|
||||
)
|
||||
methodExtractFormattingAttribute.isAccessible = true
|
||||
methodExtractFormattingAttribute.invoke(textView, context, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun setFormattingAllowed() {
|
||||
val fieldFormattingAllowed = textView!!.javaClass.getDeclaredField("formattingAllowed")
|
||||
fieldFormattingAllowed.isAccessible = true
|
||||
textView!!.setFormattingAllowed(true)
|
||||
Assert.assertTrue(fieldFormattingAllowed.getBoolean(textView))
|
||||
textView!!.setFormattingAllowed(false)
|
||||
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FileUtilsTest {
|
||||
@Test
|
||||
public void isSelfOwned() throws Exception {
|
||||
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
|
||||
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
||||
assertThat(selfOwned, is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isNotSelfOwned() throws Exception {
|
||||
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
|
||||
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
||||
assertThat(selfOwned, is(false));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package fr.free.nrw.commons.util
|
||||
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.AppCompatSpinner
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import org.hamcrest.Matcher
|
||||
|
||||
class MyViewAction {
|
||||
companion object {
|
||||
fun typeTextInChildViewWithId(
|
||||
id: Int,
|
||||
textToBeTyped: String,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
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 =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
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 =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as View
|
||||
v.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_type">fr.free.nrw.commons.beta</string>
|
||||
<string name="contribution_authority">fr.free.nrw.commons.beta.contributions.contentprovider</string>
|
||||
<string name="modification_authority">fr.free.nrw.commons.beta.modifications.contentprovider</string>
|
||||
<string name="category_authority">fr.free.nrw.commons.beta.categories.contentprovider</string>
|
||||
</resources>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_settings_black"
|
||||
android:shortcutId="Setting"
|
||||
android:shortcutLongLabel="@string/title_app_shortcut_setting"
|
||||
android:shortcutShortLabel="@string/title_app_shortcut_setting"
|
||||
tools:targetApi="n_mr1">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetClass="fr.free.nrw.commons.settings.SettingsActivity"
|
||||
android:targetPackage="fr.free.nrw.commons.beta" />
|
||||
</shortcut>
|
||||
|
||||
</shortcuts>
|
||||
|
Before Width: | Height: | Size: 19 KiB |
BIN
app/src/betaDebug/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/betaDebug/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/betaDebug/res/drawable-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
6
app/src/debug/res/values/placeholder_strings.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="placeholder_place_distance">Overlay</string>
|
||||
<string name="placeholder_place_name">Name</string>
|
||||
<string name="placeholder_place_description">Description</string>
|
||||
</resources>
|
||||
|
|
@ -1,259 +1,160 @@
|
|||
<?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"
|
||||
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" />
|
||||
<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.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||
<uses-permission android:name="android.permission.READ_LOGS"/>
|
||||
|
||||
<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="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/LightAppTheme"
|
||||
android:supportsRtl="true" >
|
||||
<activity android:name="org.acra.CrashReportDialog"
|
||||
android:theme="@android:style/Theme.Dialog"
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<activity android:name=".auth.LoginActivity">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<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=".WelcomeActivity" />
|
||||
|
||||
<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=".activity.SingleWebViewActivity"
|
||||
android:exported="false" />
|
||||
<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:windowSoftInputMode="adjustPan"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<activity
|
||||
android:name=".upload.ShareActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<activity
|
||||
android:name=".upload.MultipleShareActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<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="adjustPan">
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<activity
|
||||
android:name=".contributions.ContributionsActivity"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/ogg" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<activity
|
||||
android:name=".auth.SignupActivity"
|
||||
android:label="@string/title_activity_signup" />
|
||||
|
||||
<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" />
|
||||
<activity
|
||||
android:name=".nearby.NearbyActivity"
|
||||
android:label="@string/title_activity_nearby"
|
||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
<service android:name=".upload.UploadService" />
|
||||
|
||||
<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" />
|
||||
<service
|
||||
android:name=".auth.WikiAccountAuthenticatorService"
|
||||
android:exported="true"
|
||||
android:process=":auth">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<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.items.BookmarkItemsContentProvider"
|
||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_bookmarks_location"
|
||||
android:syncable="false" />
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.PicOfDayAppWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<service
|
||||
android:name=".contributions.ContributionsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/contributions_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||
</receiver>
|
||||
<service
|
||||
android:name=".modifications.ModificationsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/modifications_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false" />
|
||||
</application>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
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=".contributions.ContributionsContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".modifications.ModificationsContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_modifications"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="fr.free.nrw.commons.categories.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_categories"
|
||||
android:syncable="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
126
app/src/main/assets/fontconfig/fonts.conf
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<!-- /etc/fonts/fonts.conf file to configure system font access -->
|
||||
<fontconfig>
|
||||
|
||||
<!-- Font directory list -->
|
||||
|
||||
<dir prefix="xdg">fontconfig/fonts</dir>
|
||||
|
||||
<!-- Font cache directory list -->
|
||||
|
||||
<cachedir prefix="xdg">fontconfig</cachedir>
|
||||
|
||||
<!--
|
||||
Accept deprecated 'mono' alias, replacing it with 'monospace'
|
||||
-->
|
||||
<match target="pattern">
|
||||
<test qual="any" name="family">
|
||||
<string>mono</string>
|
||||
</test>
|
||||
<edit name="family" mode="assign" binding="same">
|
||||
<string>monospace</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<!--
|
||||
Accept alternate 'sans serif' spelling, replacing it with 'sans-serif'
|
||||
-->
|
||||
<match target="pattern">
|
||||
<test qual="any" name="family">
|
||||
<string>sans serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="assign" binding="same">
|
||||
<string>sans-serif</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<!--
|
||||
Accept deprecated 'sans' alias, replacing it with 'sans-serif'
|
||||
-->
|
||||
<match target="pattern">
|
||||
<test qual="any" name="family">
|
||||
<string>sans</string>
|
||||
</test>
|
||||
<edit name="family" mode="assign" binding="same">
|
||||
<string>sans-serif</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<config>
|
||||
<!--
|
||||
These are the default Unicode chars that are expected to be blank
|
||||
in fonts. All other blank chars are assumed to be broken and
|
||||
won't appear in the resulting charsets
|
||||
-->
|
||||
<blank>
|
||||
<int>0x0020</int> <!-- SPACE -->
|
||||
<int>0x00A0</int> <!-- NO-BREAK SPACE -->
|
||||
<int>0x00AD</int> <!-- SOFT HYPHEN -->
|
||||
<int>0x034F</int> <!-- COMBINING GRAPHEME JOINER -->
|
||||
<int>0x0600</int> <!-- ARABIC NUMBER SIGN -->
|
||||
<int>0x0601</int> <!-- ARABIC SIGN SANAH -->
|
||||
<int>0x0602</int> <!-- ARABIC FOOTNOTE MARKER -->
|
||||
<int>0x0603</int> <!-- ARABIC SIGN SAFHA -->
|
||||
<int>0x06DD</int> <!-- ARABIC END OF AYAH -->
|
||||
<int>0x070F</int> <!-- SYRIAC ABBREVIATION MARK -->
|
||||
<int>0x115F</int> <!-- HANGUL CHOSEONG FILLER -->
|
||||
<int>0x1160</int> <!-- HANGUL JUNGSEONG FILLER -->
|
||||
<int>0x1680</int> <!-- OGHAM SPACE MARK -->
|
||||
<int>0x17B4</int> <!-- KHMER VOWEL INHERENT AQ -->
|
||||
<int>0x17B5</int> <!-- KHMER VOWEL INHERENT AA -->
|
||||
<int>0x180E</int> <!-- MONGOLIAN VOWEL SEPARATOR -->
|
||||
<int>0x2000</int> <!-- EN QUAD -->
|
||||
<int>0x2001</int> <!-- EM QUAD -->
|
||||
<int>0x2002</int> <!-- EN SPACE -->
|
||||
<int>0x2003</int> <!-- EM SPACE -->
|
||||
<int>0x2004</int> <!-- THREE-PER-EM SPACE -->
|
||||
<int>0x2005</int> <!-- FOUR-PER-EM SPACE -->
|
||||
<int>0x2006</int> <!-- SIX-PER-EM SPACE -->
|
||||
<int>0x2007</int> <!-- FIGURE SPACE -->
|
||||
<int>0x2008</int> <!-- PUNCTUATION SPACE -->
|
||||
<int>0x2009</int> <!-- THIN SPACE -->
|
||||
<int>0x200A</int> <!-- HAIR SPACE -->
|
||||
<int>0x200B</int> <!-- ZERO WIDTH SPACE -->
|
||||
<int>0x200C</int> <!-- ZERO WIDTH NON-JOINER -->
|
||||
<int>0x200D</int> <!-- ZERO WIDTH JOINER -->
|
||||
<int>0x200E</int> <!-- LEFT-TO-RIGHT MARK -->
|
||||
<int>0x200F</int> <!-- RIGHT-TO-LEFT MARK -->
|
||||
<int>0x2028</int> <!-- LINE SEPARATOR -->
|
||||
<int>0x2029</int> <!-- PARAGRAPH SEPARATOR -->
|
||||
<int>0x202A</int> <!-- LEFT-TO-RIGHT EMBEDDING -->
|
||||
<int>0x202B</int> <!-- RIGHT-TO-LEFT EMBEDDING -->
|
||||
<int>0x202C</int> <!-- POP DIRECTIONAL FORMATTING -->
|
||||
<int>0x202D</int> <!-- LEFT-TO-RIGHT OVERRIDE -->
|
||||
<int>0x202E</int> <!-- RIGHT-TO-LEFT OVERRIDE -->
|
||||
<int>0x202F</int> <!-- NARROW NO-BREAK SPACE -->
|
||||
<int>0x205F</int> <!-- MEDIUM MATHEMATICAL SPACE -->
|
||||
<int>0x2060</int> <!-- WORD JOINER -->
|
||||
<int>0x2061</int> <!-- FUNCTION APPLICATION -->
|
||||
<int>0x2062</int> <!-- INVISIBLE TIMES -->
|
||||
<int>0x2063</int> <!-- INVISIBLE SEPARATOR -->
|
||||
<int>0x206A</int> <!-- INHIBIT SYMMETRIC SWAPPING -->
|
||||
<int>0x206B</int> <!-- ACTIVATE SYMMETRIC SWAPPING -->
|
||||
<int>0x206C</int> <!-- INHIBIT ARABIC FORM SHAPING -->
|
||||
<int>0x206D</int> <!-- ACTIVATE ARABIC FORM SHAPING -->
|
||||
<int>0x206E</int> <!-- NATIONAL DIGIT SHAPES -->
|
||||
<int>0x206F</int> <!-- NOMINAL DIGIT SHAPES -->
|
||||
<int>0x2800</int> <!-- BRAILLE PATTERN BLANK -->
|
||||
<int>0x3000</int> <!-- IDEOGRAPHIC SPACE -->
|
||||
<int>0x3164</int> <!-- HANGUL FILLER -->
|
||||
<int>0xFEFF</int> <!-- ZERO WIDTH NO-BREAK SPACE -->
|
||||
<int>0xFFA0</int> <!-- HALFWIDTH HANGUL FILLER -->
|
||||
<int>0xFFF9</int> <!-- INTERLINEAR ANNOTATION ANCHOR -->
|
||||
<int>0xFFFA</int> <!-- INTERLINEAR ANNOTATION SEPARATOR -->
|
||||
<int>0xFFFB</int> <!-- INTERLINEAR ANNOTATION TERMINATOR -->
|
||||
</blank>
|
||||
<!--
|
||||
Rescan configuration every 30 seconds when FcFontSetList is called
|
||||
-->
|
||||
<rescan>
|
||||
<int>30</int>
|
||||
</rescan>
|
||||
</config>
|
||||
|
||||
</fontconfig>
|
||||
|
||||
BIN
app/src/main/assets/fontconfig/fonts/truetype/Ubuntu-R.ttf
Normal file
26
app/src/main/assets/mapstyle.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"wikimedia-osm": {
|
||||
"type": "raster",
|
||||
"tiles": [
|
||||
"https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
|
||||
],
|
||||
"tileSize": 128
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "#606060"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "osm",
|
||||
"type": "raster",
|
||||
"source": "wikimedia-osm"
|
||||
}
|
||||
]
|
||||
}
|
||||
55
app/src/main/assets/queries/nearby_query.rq
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
SELECT
|
||||
(SAMPLE(?location) as ?location)
|
||||
?item
|
||||
(SAMPLE(COALESCE(?item_label_preferred_language, ?item_label_any_language)) as ?label)
|
||||
(SAMPLE(?classId) as ?class)
|
||||
(SAMPLE(COALESCE(?class_label_preferred_language, ?class_label_any_language, "?")) as ?class_label)
|
||||
(SAMPLE(COALESCE(?icon0, ?icon1)) as ?icon)
|
||||
(SAMPLE(COALESCE(?emoji0, ?emoji1)) as ?emoji)
|
||||
?wikipediaArticle
|
||||
?commonsArticle
|
||||
WHERE {
|
||||
# Around given location...
|
||||
SERVICE wikibase:around {
|
||||
?item wdt:P625 ?location.
|
||||
bd:serviceParam wikibase:center "Point(${LONG} ${LAT})"^^geo:wktLiteral.
|
||||
bd:serviceParam wikibase:radius "${RAD}" . # Radius in kilometers.
|
||||
}
|
||||
|
||||
# ... and without an image.
|
||||
MINUS {?item wdt:P18 []}
|
||||
|
||||
# Get the label in the preferred language of the user, or any other language if no label is available in that language.
|
||||
OPTIONAL {?item rdfs:label ?item_label_preferred_language. FILTER (lang(?item_label_preferred_language) = "${LANG}")}
|
||||
OPTIONAL {?item rdfs:label ?item_label_any_language}
|
||||
|
||||
# Get the class label in the preferred language of the user, or any other language if no label is available in that language.
|
||||
OPTIONAL {
|
||||
?item p:P31/ps:P31 ?classId.
|
||||
OPTIONAL {?classId rdfs:label ?class_label_preferred_language. FILTER (lang(?class_label_preferred_language) = "${LANG}")}
|
||||
OPTIONAL {?classId rdfs:label ?class_label_any_language}
|
||||
|
||||
# Get icon
|
||||
OPTIONAL { ?classId wdt:P2910 ?icon0. }
|
||||
OPTIONAL { ?classId wdt:P279*/wdt:P2910 ?icon1. }
|
||||
# Get emoji
|
||||
OPTIONAL { ?classId wdt:P487 ?emoji0. }
|
||||
OPTIONAL { ?classId wdt:P279*/wdt:P487 ?emoji1. }
|
||||
OPTIONAL {
|
||||
?sitelink schema:about ?item .
|
||||
?sitelink schema:inLanguage "en"
|
||||
}
|
||||
OPTIONAL {
|
||||
?wikipediaArticle schema:about ?item ;
|
||||
schema:isPartOf <https://en.wikipedia.org/> .
|
||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
|
||||
}
|
||||
|
||||
OPTIONAL {
|
||||
?commonsArticle schema:about ?item ;
|
||||
schema:isPartOf <https://commons.wikimedia.org/> .
|
||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
|
||||
}
|
||||
}
|
||||
}
|
||||
GROUP BY ?item ?wikipediaArticle ?commonsArticle
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 16 KiB |
28
app/src/main/java/fr/free/nrw/commons/AboutActivity.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
|
||||
public class AboutActivity extends NavigationBaseActivity {
|
||||
@BindView(R.id.about_version) TextView versionText;
|
||||
@BindView(R.id.about_license) HtmlTextView aboutLicenseText;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name));
|
||||
aboutLicenseText.setHtmlText(aboutText);
|
||||
|
||||
versionText.setText(BuildConfig.VERSION_NAME);
|
||||
initDrawer();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
||||
import fr.free.nrw.commons.databinding.ActivityAboutBinding
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||
import java.util.Collections
|
||||
import androidx.core.net.toUri
|
||||
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
|
||||
import fr.free.nrw.commons.utils.handleWebUrl
|
||||
import fr.free.nrw.commons.utils.setUnderlinedText
|
||||
|
||||
/**
|
||||
* Represents about screen of this app
|
||||
*/
|
||||
class AboutActivity : BaseActivity() {
|
||||
/*
|
||||
This View Binding class is auto-generated for each xml file. The format is usually the name
|
||||
of the file with PascalCasing (The underscore characters will be ignored).
|
||||
More information is available at https://developer.android.com/topic/libraries/view-binding
|
||||
*/
|
||||
private var binding: ActivityAboutBinding? = null
|
||||
|
||||
/**
|
||||
* This method helps in the creation About screen
|
||||
*
|
||||
* @param savedInstanceState Data bundle
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid") //TODO:
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
/*
|
||||
Instead of just setting the view with the xml file. We need to use View Binding class.
|
||||
*/
|
||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
val view: View = binding!!.root
|
||||
applyEdgeToEdgeTopInsets(binding!!.toolbarLayout)
|
||||
setContentView(view)
|
||||
|
||||
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
val aboutText = getString(R.string.about_license)
|
||||
/*
|
||||
We can then access all the views by just using the id names like this.
|
||||
camelCasing is used with underscore characters being ignored.
|
||||
*/
|
||||
binding!!.aboutLicense.setHtmlText(aboutText)
|
||||
|
||||
@SuppressLint("StringFormatMatches") // TODO:
|
||||
val improveText =
|
||||
String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL)
|
||||
binding!!.aboutImprove.setHtmlText(improveText)
|
||||
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
|
||||
|
||||
binding!!.aboutFaq.setUnderlinedText(R.string.about_faq)
|
||||
binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us)
|
||||
binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide)
|
||||
binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy)
|
||||
binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate)
|
||||
binding!!.aboutCredits.setUnderlinedText(R.string.about_credits)
|
||||
|
||||
/*
|
||||
To set listeners, we can create a separate method and use lambda syntax.
|
||||
*/
|
||||
binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook)
|
||||
binding!!.githubLaunchIcon.setOnClickListener(::launchGithub)
|
||||
binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite)
|
||||
binding!!.aboutRateUs.setOnClickListener(::launchRatings)
|
||||
binding!!.aboutCredits.setOnClickListener(::launchCredits)
|
||||
binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy)
|
||||
binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide)
|
||||
binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions)
|
||||
binding!!.aboutTranslate.setOnClickListener(::launchTranslate)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
fun launchFacebook(view: View?) {
|
||||
val intent: Intent
|
||||
try {
|
||||
intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri())
|
||||
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
fun launchGithub(view: View?) {
|
||||
val intent: Intent
|
||||
try {
|
||||
intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri())
|
||||
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
fun launchWebsite(view: View?) {
|
||||
handleWebUrl(this, Urls.WEBSITE_URL.toUri())
|
||||
}
|
||||
|
||||
fun launchRatings(view: View?) {
|
||||
try {
|
||||
startActivity(
|
||||
Intent(
|
||||
ACTION_VIEW,
|
||||
(Urls.PLAY_STORE_PREFIX + packageName).toUri()
|
||||
)
|
||||
)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri())
|
||||
}
|
||||
}
|
||||
|
||||
fun launchCredits(view: View?) {
|
||||
handleWebUrl(this, Urls.CREDITS_URL.toUri())
|
||||
}
|
||||
|
||||
fun launchUserGuide(view: View?) {
|
||||
handleWebUrl(this, Urls.USER_GUIDE_URL.toUri())
|
||||
}
|
||||
|
||||
fun launchPrivacyPolicy(view: View?) {
|
||||
handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri())
|
||||
}
|
||||
|
||||
fun launchFrequentlyAskedQuesions(view: View?) {
|
||||
handleWebUrl(this, Urls.FAQ_URL.toUri())
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.menu_about, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.share_app_icon -> {
|
||||
val shareText = String.format(
|
||||
getString(R.string.share_text),
|
||||
Urls.PLAY_STORE_URL_PREFIX + this.packageName
|
||||
)
|
||||
val sendIntent = Intent()
|
||||
sendIntent.setAction(Intent.ACTION_SEND)
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText)
|
||||
sendIntent.setType("text/plain")
|
||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)))
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchTranslate(view: View?) {
|
||||
val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames()
|
||||
Collections.sort(sortedLocalizedNamesRef)
|
||||
val languageAdapter = ArrayAdapter(
|
||||
this@AboutActivity,
|
||||
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef
|
||||
)
|
||||
val spinner = Spinner(this@AboutActivity)
|
||||
spinner.layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
spinner.adapter = languageAdapter
|
||||
spinner.gravity = 17
|
||||
spinner.setPadding(50, 0, 0, 0)
|
||||
|
||||
val positiveButtonRunnable = Runnable {
|
||||
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
|
||||
handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri())
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
/**
|
||||
* Base presenter, enforcing contracts to attach and detach view
|
||||
*/
|
||||
interface BasePresenter<T> {
|
||||
fun onAttachView(view: T)
|
||||
|
||||
fun onDetachView()
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
/**
|
||||
* Production variant related constants which is used in beta variant for some specific GET calls on
|
||||
* production server where beta server does not work
|
||||
*/
|
||||
object BetaConstants {
|
||||
/**
|
||||
* Commons production URL which is used in beta for some specific GET calls on
|
||||
* 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"
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DaggerApplication;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationComponent;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.utils.FileUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
// TODO: Use ProGuard to rip out reporting when publishing
|
||||
@ReportsCrashes(
|
||||
mailTo = "commons-app-android-private@googlegroups.com",
|
||||
mode = ReportingInteractionMode.DIALOG,
|
||||
resDialogText = R.string.crash_dialog_text,
|
||||
resDialogTitle = R.string.crash_dialog_title,
|
||||
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
|
||||
resDialogOkToast = R.string.crash_dialog_ok_toast
|
||||
)
|
||||
public class CommonsApplication extends DaggerApplication {
|
||||
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
|
||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
||||
@Inject @Named("prefs") SharedPreferences otherPrefs;
|
||||
|
||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
|
||||
|
||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
||||
public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
|
||||
|
||||
private CommonsApplicationComponent component;
|
||||
private RefWatcher refWatcher;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Fresco.initialize(this);
|
||||
if (setupLeakCanary() == RefWatcher.DISABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
ACRA.init(this);
|
||||
} else {
|
||||
Stetho.initializeWithDefaults(this);
|
||||
}
|
||||
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||
}
|
||||
|
||||
protected RefWatcher setupLeakCanary() {
|
||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
return RefWatcher.DISABLED;
|
||||
}
|
||||
return LeakCanary.install(this);
|
||||
}
|
||||
|
||||
public static RefWatcher getRefWatcher(Context context) {
|
||||
CommonsApplication application = (CommonsApplication) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
|
||||
return injector();
|
||||
}
|
||||
|
||||
public CommonsApplicationComponent injector() {
|
||||
if (component == null) {
|
||||
component = DaggerCommonsApplicationComponent.builder()
|
||||
.appModule(new CommonsApplicationModule(this))
|
||||
.build();
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
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.clearAllAccounts()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
Timber.d("All accounts have been removed");
|
||||
//TODO: fix preference manager
|
||||
defaultPrefs.edit().clear().commit();
|
||||
applicationPrefs.edit().clear().commit();
|
||||
applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit();
|
||||
updateAllDatabases();
|
||||
|
||||
logoutListener.onLogoutComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tables and re-creates them.
|
||||
*/
|
||||
private void updateAllDatabases() {
|
||||
dbOpenHelper.getReadableDatabase().close();
|
||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
ModifierSequence.Table.onDelete(db);
|
||||
Category.Table.onDelete(db);
|
||||
Contribution.Table.onDelete(db);
|
||||
}
|
||||
|
||||
public interface LogoutListener {
|
||||
void onLogoutComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
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.BookmarkItemsTable
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
|
||||
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
|
||||
|
||||
dbOpenHelper.deleteTable(
|
||||
db,
|
||||
DBOpenHelper.BOOKMARKS_LOCATIONS
|
||||
)
|
||||
|
||||
try {
|
||||
contributionDao.deleteAll()
|
||||
} catch (e: SQLiteException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
BookmarksTable.onDelete(db)
|
||||
BookmarkItemsTable.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
app/src/main/java/fr/free/nrw/commons/HandlerService.java
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import dagger.android.DaggerService;
|
||||
|
||||
public abstract class HandlerService<T> extends DaggerService {
|
||||
private volatile Looper threadLooper;
|
||||
private volatile ServiceHandler threadHandler;
|
||||
private String serviceName;
|
||||
|
||||
private final class ServiceHandler extends Handler {
|
||||
public ServiceHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
//FIXME: Google Photos bug
|
||||
handle(msg.what, (T)msg.obj);
|
||||
stopSelf(msg.arg1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
threadLooper.quit();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public class HandlerServiceLocalBinder extends Binder {
|
||||
public HandlerService getService() {
|
||||
return HandlerService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private final IBinder localBinder = new HandlerServiceLocalBinder();
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return localBinder;
|
||||
}
|
||||
|
||||
protected HandlerService(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
HandlerThread thread = new HandlerThread(serviceName);
|
||||
thread.start();
|
||||
|
||||
threadLooper = thread.getLooper();
|
||||
threadHandler = new ServiceHandler(threadLooper);
|
||||
}
|
||||
|
||||
private void postMessage(int type, T t) {
|
||||
Message msg = threadHandler.obtainMessage(type);
|
||||
msg.obj = t;
|
||||
threadHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
public void queue(int what, T t) {
|
||||
postMessage(what, t);
|
||||
}
|
||||
|
||||
protected abstract void handle(int what, T t);
|
||||
}
|
||||
66
app/src/main/java/fr/free/nrw/commons/License.java
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class License {
|
||||
private String key;
|
||||
private String template;
|
||||
private String url;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of License.
|
||||
*
|
||||
* @param key license key
|
||||
* @param template license template
|
||||
* @param url license URL
|
||||
* @param name licence name
|
||||
*
|
||||
* @throws RuntimeException if License.key or Licence.template is null
|
||||
*/
|
||||
public License(String key, String template, String url, String name) {
|
||||
if (key == null) {
|
||||
throw new RuntimeException("License.key must not be null");
|
||||
}
|
||||
if (template == null) {
|
||||
throw new RuntimeException("License.template must not be null");
|
||||
}
|
||||
this.key = key;
|
||||
this.template = template;
|
||||
this.url = url;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license key.
|
||||
* @return license key as a String.
|
||||
*/
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license template.
|
||||
* @return license template as a String.
|
||||
*/
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (name == null) {
|
||||
// hack
|
||||
return getKey();
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String getUrl(String language) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
} else {
|
||||
return url.replace("$lang", language);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
app/src/main/java/fr/free/nrw/commons/LicenseList.java
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Resources;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class LicenseList {
|
||||
private Map<String, License> licenses = new HashMap<>();
|
||||
private Resources res;
|
||||
|
||||
public LicenseList(Activity activity) {
|
||||
res = activity.getResources();
|
||||
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
|
||||
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
|
||||
while (xmlFastForward(parser, namespace, "license")) {
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String template = parser.getAttributeValue(null, "template");
|
||||
String url = parser.getAttributeValue(null, "url");
|
||||
String name = nameForTemplate(template);
|
||||
License license = new License(id, template, url, name);
|
||||
licenses.put(id, license);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<License> values() {
|
||||
return licenses.values();
|
||||
}
|
||||
|
||||
public License get(String key) {
|
||||
return licenses.get(key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
License licenseForTemplate(String template) {
|
||||
String ucTemplate = new PageTitle(template).getDisplayText();
|
||||
for (License license : values()) {
|
||||
if (ucTemplate.equals(new PageTitle(license.getTemplate()).getDisplayText())) {
|
||||
return license;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String nameIdForTemplate(String template) {
|
||||
// hack :D (converts dashes and periods to underscores)
|
||||
// cc-by-sa-3.0 -> cc_by_sa_3_0
|
||||
return "license_name_" + template.toLowerCase(Locale.ENGLISH).replace("-",
|
||||
"_").replace(".", "_");
|
||||
}
|
||||
|
||||
private String nameForTemplate(String template) {
|
||||
int nameId = res.getIdentifier("fr.free.nrw.commons:string/"
|
||||
+ nameIdForTemplate(template), null, null);
|
||||
return (nameId != 0) ? res.getString(nameId) : template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-forward an XmlPullParser to the next instance of the given element
|
||||
* in the input stream (namespaced).
|
||||
*
|
||||
* @param parser
|
||||
* @param namespace
|
||||
* @param element
|
||||
* @return true on match, false on failure
|
||||
*/
|
||||
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
|
||||
try {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.getEventType() == XmlPullParser.START_TAG &&
|
||||
parser.getNamespace().equals(namespace) &&
|
||||
parser.getName().equals(element)) {
|
||||
// We found it!
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
|
||||
abstract class MapController {
|
||||
/**
|
||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||
*/
|
||||
inner class NearbyPlacesInfo {
|
||||
@JvmField
|
||||
var placeList: List<Place> = emptyList() // List of nearby places
|
||||
|
||||
@JvmField
|
||||
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
|
||||
|
||||
@JvmField
|
||||
var currentLatLng: LatLng? = null // Current location when this places are populated
|
||||
|
||||
@JvmField
|
||||
var searchLatLng: LatLng? = null // Search location for finding this places
|
||||
|
||||
@JvmField
|
||||
var mediaList: List<Media>? = null // Search location for finding this places
|
||||
}
|
||||
|
||||
/**
|
||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||
*/
|
||||
inner class ExplorePlacesInfo {
|
||||
@JvmField
|
||||
var explorePlaceList: List<Place> = emptyList() // List of nearby places
|
||||
|
||||
@JvmField
|
||||
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
|
||||
|
||||
@JvmField
|
||||
var currentLatLng: LatLng? = null // Current location when this places are populated
|
||||
|
||||
@JvmField
|
||||
var searchLatLng: LatLng? = null // Search location for finding this places
|
||||
|
||||
@JvmField
|
||||
var mediaList: List<Media> = emptyList() // Search location for finding this places
|
||||
}
|
||||
}
|
||||
354
app/src/main/java/fr/free/nrw/commons/Media.java
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
|
||||
public class Media implements Parcelable {
|
||||
|
||||
public static Creator<Media> CREATOR = new Creator<Media>() {
|
||||
@Override
|
||||
public Media createFromParcel(Parcel parcel) {
|
||||
return new Media(parcel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Media[] newArray(int i) {
|
||||
return new Media[0];
|
||||
}
|
||||
};
|
||||
|
||||
private static Pattern displayTitlePattern = Pattern.compile("(.*)(\\.\\w+)", Pattern.CASE_INSENSITIVE);
|
||||
// Primary metadata fields
|
||||
protected Uri localUri;
|
||||
protected String imageUrl;
|
||||
protected String filename;
|
||||
protected String description; // monolingual description on input...
|
||||
protected long dataLength;
|
||||
protected Date dateCreated;
|
||||
protected @Nullable Date dateUploaded;
|
||||
protected int width;
|
||||
protected int height;
|
||||
protected String license;
|
||||
protected String creator;
|
||||
protected ArrayList<String> categories; // as loaded at runtime?
|
||||
private Map<String, String> descriptions; // multilingual descriptions as loaded
|
||||
private HashMap<String, Object> tags = new HashMap<>();
|
||||
private @Nullable LatLng coordinates;
|
||||
|
||||
protected Media() {
|
||||
this.categories = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
}
|
||||
|
||||
public Media(String filename) {
|
||||
this();
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public Media(Uri localUri, String imageUrl, String filename, String description,
|
||||
long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) {
|
||||
this();
|
||||
this.localUri = localUri;
|
||||
this.imageUrl = imageUrl;
|
||||
this.filename = filename;
|
||||
this.description = description;
|
||||
this.dataLength = dataLength;
|
||||
this.dateCreated = dateCreated;
|
||||
this.dateUploaded = dateUploaded;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Media(Parcel in) {
|
||||
localUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
imageUrl = in.readString();
|
||||
filename = in.readString();
|
||||
description = in.readString();
|
||||
dataLength = in.readLong();
|
||||
dateCreated = (Date) in.readSerializable();
|
||||
dateUploaded = (Date) in.readSerializable();
|
||||
creator = in.readString();
|
||||
tags = (HashMap<String, Object>) in.readSerializable();
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
license = in.readString();
|
||||
if (categories != null) {
|
||||
in.readStringList(categories);
|
||||
}
|
||||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
|
||||
public Object getTag(String key) {
|
||||
return tags.get(key);
|
||||
}
|
||||
|
||||
public void setTag(String key, Object value) {
|
||||
tags.put(key, value);
|
||||
}
|
||||
|
||||
public String getDisplayTitle() {
|
||||
if (filename == null) {
|
||||
return "";
|
||||
}
|
||||
// FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows
|
||||
String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", "");
|
||||
Matcher matcher = displayTitlePattern.matcher(title);
|
||||
if (matcher.matches()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
public PageTitle getFilePageTitle() {
|
||||
return new PageTitle("File:" + getFilename().replaceFirst("^File:", ""));
|
||||
}
|
||||
|
||||
public Uri getLocalUri() {
|
||||
return localUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getImageUrl() {
|
||||
if (imageUrl == null && this.getFilename() != null) {
|
||||
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the file.
|
||||
* @return file name as a string
|
||||
*/
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the file.
|
||||
* @param filename the new name of the file
|
||||
*/
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file description.
|
||||
* @return file description as a string
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param description the new description of the file
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the datalength of the file.
|
||||
* @return file datalength as a long
|
||||
*/
|
||||
public long getDataLength() {
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the datalength of the file.
|
||||
* @param dataLength as a long
|
||||
*/
|
||||
public void setDataLength(long dataLength) {
|
||||
this.dataLength = dataLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the creation date of the file.
|
||||
* @return creation date as a Date
|
||||
*/
|
||||
public Date getDateCreated() {
|
||||
return dateCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the creation date of the file.
|
||||
* @param date creation date as a Date
|
||||
*/
|
||||
public void setDateCreated(Date date) {
|
||||
this.dateCreated = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upload date of the file.
|
||||
* Can be null.
|
||||
* @return upload date as a Date
|
||||
*/
|
||||
public @Nullable
|
||||
Date getDateUploaded() {
|
||||
return dateUploaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the creator of the file.
|
||||
* @return creator name as a String
|
||||
*/
|
||||
public String getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the creator name of the file.
|
||||
* @param creator creator name as a string
|
||||
*/
|
||||
public void setCreator(String creator) {
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the media.
|
||||
* @return file width as an int
|
||||
*/
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the media.
|
||||
* @param width file width as an int
|
||||
*/
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the media.
|
||||
* @return file height as an int
|
||||
*/
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the media.
|
||||
* @param height file height as an int
|
||||
*/
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the license name of the file.
|
||||
* @return license as a String
|
||||
*/
|
||||
public String getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
* @param license license name as a String
|
||||
*/
|
||||
public void setLicense(String license) {
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the coordinates of where the file was created.
|
||||
* @return file coordinates as a LatLng
|
||||
*/
|
||||
public @Nullable
|
||||
LatLng getCoordinates() {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinates of where the file was created.
|
||||
* @param coordinates file coordinates as a LatLng
|
||||
*/
|
||||
public void setCoordinates(@Nullable LatLng coordinates) {
|
||||
this.coordinates = coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ArrayList<String> getCategories() {
|
||||
return (ArrayList<String>) categories.clone(); // feels dirty
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the categories the file falls under.
|
||||
* </p>
|
||||
* Does not append: i.e. will clear the current categories
|
||||
* and then add the specified ones.
|
||||
* @param categories file categories as a list of Strings
|
||||
*/
|
||||
public void setCategories(List<String> categories) {
|
||||
this.categories.clear();
|
||||
this.categories.addAll(categories);
|
||||
}
|
||||
|
||||
void setDescriptions(Map<String, String> descriptions) {
|
||||
for (String key : this.descriptions.keySet()) {
|
||||
this.descriptions.remove(key);
|
||||
}
|
||||
for (String key : descriptions.keySet()) {
|
||||
this.descriptions.put(key, descriptions.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription(String preferredLanguage) {
|
||||
if (descriptions.containsKey(preferredLanguage)) {
|
||||
// See if the requested language is there.
|
||||
return descriptions.get(preferredLanguage);
|
||||
} else if (descriptions.containsKey("en")) {
|
||||
// Ah, English. Language of the world, until the Chinese crush us.
|
||||
return descriptions.get("en");
|
||||
} else if (descriptions.containsKey("default")) {
|
||||
// No languages marked...
|
||||
return descriptions.get("default");
|
||||
} else {
|
||||
// FIXME: return the first available non-English description?
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeParcelable(localUri, flags);
|
||||
parcel.writeString(imageUrl);
|
||||
parcel.writeString(filename);
|
||||
parcel.writeString(description);
|
||||
parcel.writeLong(dataLength);
|
||||
parcel.writeSerializable(dateCreated);
|
||||
parcel.writeSerializable(dateUploaded);
|
||||
parcel.writeString(creator);
|
||||
parcel.writeSerializable(tags);
|
||||
parcel.writeInt(width);
|
||||
parcel.writeInt(height);
|
||||
parcel.writeString(license);
|
||||
parcel.writeStringList(categories);
|
||||
parcel.writeMap(descriptions);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.os.Parcelable
|
||||
import fr.free.nrw.commons.BuildConfig.COMMONS_URL
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||
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(
|
||||
/**
|
||||
* @return pageId for the current media object
|
||||
* Wikibase Identifier associated with media files
|
||||
*/
|
||||
var pageId: String = UUID.randomUUID().toString(),
|
||||
var thumbUrl: String? = null,
|
||||
/**
|
||||
* Gets image URL
|
||||
* @return Image URL
|
||||
*/
|
||||
var imageUrl: String? = null,
|
||||
/**
|
||||
* Gets the name of the file.
|
||||
* @return file name as a string
|
||||
*/
|
||||
var filename: String? = null,
|
||||
/**
|
||||
* The fallback description of the file, used if no other description is provided.
|
||||
*/
|
||||
var fallbackDescription: String? = null,
|
||||
/**
|
||||
* Gets the upload date of the file.
|
||||
* Can be null.
|
||||
* @return upload date as a Date
|
||||
*/
|
||||
var dateUploaded: Date? = null,
|
||||
/**
|
||||
* The license name of the file.
|
||||
*/
|
||||
var license: String? = null,
|
||||
/**
|
||||
* The URL corresponding to the license.
|
||||
*/
|
||||
var licenseUrl: String? = null,
|
||||
/**
|
||||
* The name of the creator of the file.
|
||||
*/
|
||||
var author: String? = null,
|
||||
/**
|
||||
* The username of the uploader.
|
||||
*/
|
||||
var user: String? = null,
|
||||
/**
|
||||
* The full name of the file's creator, if different from username.
|
||||
*/
|
||||
var creatorName: String? = null,
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
*/
|
||||
var categories: List<String>? = null,
|
||||
/**
|
||||
* Gets the coordinates of where the file was created.
|
||||
* @return file coordinates as a LatLng
|
||||
*/
|
||||
var coordinates: LatLng? = null,
|
||||
var captions: Map<String, String> = emptyMap(),
|
||||
var descriptions: Map<String, String> = emptyMap(),
|
||||
var depictionIds: List<String> = emptyList(),
|
||||
var creatorIds: 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?,
|
||||
) : this(
|
||||
filename = filename,
|
||||
fallbackDescription = fallbackDescription,
|
||||
dateUploaded = Date(),
|
||||
author = author,
|
||||
user = user,
|
||||
categories = categories,
|
||||
captions = captions,
|
||||
)
|
||||
|
||||
constructor(
|
||||
captions: Map<String, String>,
|
||||
categories: List<String>?,
|
||||
filename: String?,
|
||||
fallbackDescription: String?,
|
||||
author: String?,
|
||||
user: String?,
|
||||
dateUploaded: Date? = Date(),
|
||||
license: String? = null,
|
||||
licenseUrl: String? = null,
|
||||
imageUrl: String? = null,
|
||||
thumbUrl: String? = null,
|
||||
coordinates: LatLng? = null,
|
||||
descriptions: Map<String, String> = emptyMap(),
|
||||
depictionIds: List<String> = emptyList(),
|
||||
categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
||||
) : this(
|
||||
pageId = UUID.randomUUID().toString(),
|
||||
filename = filename,
|
||||
fallbackDescription = fallbackDescription,
|
||||
dateUploaded = dateUploaded,
|
||||
author = author,
|
||||
user = user,
|
||||
categories = categories,
|
||||
captions = captions,
|
||||
license = license,
|
||||
licenseUrl = licenseUrl,
|
||||
imageUrl = imageUrl,
|
||||
thumbUrl = thumbUrl,
|
||||
coordinates = coordinates,
|
||||
descriptions = descriptions,
|
||||
depictionIds = depictionIds,
|
||||
categoriesHiddenStatus = categoriesHiddenStatus
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns Author if it's not null or empty, otherwise
|
||||
* returns user
|
||||
* @return Author or User
|
||||
*/
|
||||
@Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.")
|
||||
fun getAuthorOrUser(): String? {
|
||||
return if (!author.isNullOrEmpty()) {
|
||||
author
|
||||
} else{
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns author if it's not null or empty, otherwise
|
||||
* returns creator name
|
||||
* @return name of author or creator
|
||||
*/
|
||||
fun getAttributedAuthor(): String? {
|
||||
return if (!author.isNullOrEmpty()) {
|
||||
author
|
||||
} else{
|
||||
creatorName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets media display title
|
||||
* @return Media title
|
||||
*/
|
||||
val displayTitle: String
|
||||
get() =
|
||||
if (filename != null) {
|
||||
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file page title
|
||||
* @return New media page title
|
||||
*/
|
||||
val pageTitle: PageTitle
|
||||
get() = PageTitle(filename!!, WikiSite(COMMONS_URL))
|
||||
|
||||
/**
|
||||
* Returns wikicode to use the media file on a MediaWiki site
|
||||
* @return
|
||||
*/
|
||||
val wikiCode: String
|
||||
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
||||
|
||||
val mostRelevantCaption: String
|
||||
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
|
||||
}
|
||||
302
app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.mwapi.MediaResult;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Fetch additional media data from the network that we don't store locally.
|
||||
*
|
||||
* This includes things like category lists and multilingual descriptions,
|
||||
* which are not intrinsic to the media and may change due to editing.
|
||||
*/
|
||||
public class MediaDataExtractor {
|
||||
private final MediaWikiApi mediaWikiApi;
|
||||
private boolean fetched;
|
||||
private ArrayList<String> categories;
|
||||
private Map<String, String> descriptions;
|
||||
private String license;
|
||||
private @Nullable LatLng coordinates;
|
||||
|
||||
@Inject
|
||||
public MediaDataExtractor(MediaWikiApi mwApi) {
|
||||
this.categories = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
this.fetched = false;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
/*
|
||||
* Actually fetch the data over the network.
|
||||
* todo: use local caching?
|
||||
*
|
||||
* Warning: synchronous i/o, call on a background thread
|
||||
*/
|
||||
public void fetch(String filename, LicenseList licenseList) throws IOException {
|
||||
if (fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||
}
|
||||
|
||||
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
|
||||
|
||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||
extractCategories(result.getWikiSource());
|
||||
|
||||
// Description template info is extracted from preprocessor XML
|
||||
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We could fetch all category links from API, but we actually only want the ones
|
||||
* directly in the page source so they're editable. In the future this may change.
|
||||
*
|
||||
* @param source wikitext source code
|
||||
*/
|
||||
private void extractCategories(String source) {
|
||||
Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = regex.matcher(source);
|
||||
while (matcher.find()) {
|
||||
String cat = matcher.group(1).trim();
|
||||
categories.add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
|
||||
Document doc;
|
||||
try {
|
||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalStateException | SAXException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
Node templateNode = findTemplate(doc.getDocumentElement(), "information");
|
||||
if (templateNode != null) {
|
||||
Node descriptionNode = findTemplateParameter(templateNode, "description");
|
||||
descriptions = getMultilingualText(descriptionNode);
|
||||
|
||||
Node authorNode = findTemplateParameter(templateNode, "author");
|
||||
}
|
||||
|
||||
Node coordinateTemplateNode = findTemplate(doc.getDocumentElement(), "location");
|
||||
|
||||
if (coordinateTemplateNode != null) {
|
||||
coordinates = getCoordinates(coordinateTemplateNode);
|
||||
} else {
|
||||
coordinates = null;
|
||||
}
|
||||
|
||||
/*
|
||||
Pull up the license data list...
|
||||
look for the templates in two ways:
|
||||
* look for 'self' template and check its first parameter
|
||||
* if none, look for any of the known templates
|
||||
*/
|
||||
Timber.d("MediaDataExtractor searching for license");
|
||||
Node selfLicenseNode = findTemplate(doc.getDocumentElement(), "self");
|
||||
if (selfLicenseNode != null) {
|
||||
Node firstNode = findTemplateParameter(selfLicenseNode, 1);
|
||||
String licenseTemplate = getFlatText(firstNode);
|
||||
License license = licenseList.licenseForTemplate(licenseTemplate);
|
||||
if (license == null) {
|
||||
Timber.d("MediaDataExtractor found no matching license for self parameter: %s; faking it", licenseTemplate);
|
||||
this.license = licenseTemplate; // hack hack! For non-selectable licenses that are still in the system.
|
||||
} else {
|
||||
// fixme: record the self-ness in here too... sigh
|
||||
// all this needs better server-side metadata
|
||||
this.license = license.getKey();
|
||||
Timber.d("MediaDataExtractor found self-license %s", this.license);
|
||||
}
|
||||
} else {
|
||||
for (License license : licenseList.values()) {
|
||||
String templateName = license.getTemplate();
|
||||
Node template = findTemplate(doc.getDocumentElement(), templateName);
|
||||
if (template != null) {
|
||||
// Found!
|
||||
this.license = license.getKey();
|
||||
Timber.d("MediaDataExtractor found non-self license %s", this.license);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node findTemplate(Element parentNode, String title_) throws IOException {
|
||||
String title = new PageTitle(title_).getDisplayText();
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
String foundTitle = getTemplateTitle(node);
|
||||
if (title.equals(new PageTitle(foundTitle).getDisplayText())) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getTemplateTitle(Node templateNode) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("title")) {
|
||||
return node.getTextContent().trim();
|
||||
}
|
||||
}
|
||||
throw new IOException("Template has no title element.");
|
||||
}
|
||||
|
||||
private static abstract class TemplateChildNodeComparator {
|
||||
public abstract boolean match(Node node);
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
|
||||
final String theName = name;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
return (Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, int index) throws IOException {
|
||||
final String theIndex = "" + index;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
Element el = (Element)node;
|
||||
if (el.getTextContent().trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else if (el.getAttribute("index") != null && el.getAttribute("index").trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, TemplateChildNodeComparator comparator) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("part")) {
|
||||
NodeList childNodes = node.getChildNodes();
|
||||
for (int j = 0, childNodesLength = childNodes.getLength(); j < childNodesLength; j++) {
|
||||
Node childNode = childNodes.item(j);
|
||||
if (childNode.getNodeName().equals("name") && comparator.match(childNode)) {
|
||||
// yay! Now fetch the value node.
|
||||
for (int k = j + 1; k < childNodesLength; k++) {
|
||||
Node siblingNode = childNodes.item(k);
|
||||
if (siblingNode.getNodeName().equals("value")) {
|
||||
return siblingNode;
|
||||
}
|
||||
}
|
||||
throw new IOException("No value node found for matched template parameter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IOException("No matching template parameter node found.");
|
||||
}
|
||||
|
||||
private String getFlatText(Node parentNode) throws IOException {
|
||||
return parentNode.getTextContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the coordinates from the template.
|
||||
* Loops over the children of the coordinate template:
|
||||
* {{Location|47.50111007666667|19.055700301944444}}
|
||||
* and extracts the latitude and longitude.
|
||||
*
|
||||
* @param parentNode The node of the coordinates template.
|
||||
* @return Extracted coordinates.
|
||||
* @throws IOException Parsing failed.
|
||||
*/
|
||||
private LatLng getCoordinates(Node parentNode) throws IOException {
|
||||
NodeList childNodes = parentNode.getChildNodes();
|
||||
double latitudeText = Double.parseDouble(childNodes.item(1).getTextContent());
|
||||
double longitudeText = Double.parseDouble(childNodes.item(2).getTextContent());
|
||||
return new LatLng(latitudeText, longitudeText, 0);
|
||||
}
|
||||
|
||||
// Extract a dictionary of multilingual texts from a subset of the parse tree.
|
||||
// Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
|
||||
// Text outside those wrappers is stuffed into a 'default' faux language key if present.
|
||||
private Map<String, String> getMultilingualText(Node parentNode) throws IOException {
|
||||
Map<String, String> texts = new HashMap<>();
|
||||
StringBuilder localText = new StringBuilder();
|
||||
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0, length = nodes.getLength(); i < length; i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
// process a template node
|
||||
String title = getTemplateTitle(node);
|
||||
if (title.length() < 3) {
|
||||
// Hopefully a language code. Nasty hack!
|
||||
String lang = title;
|
||||
Node valueNode = findTemplateParameter(node, 1);
|
||||
String value = valueNode.getTextContent(); // hope there's no subtemplates or formatting for now
|
||||
texts.put(lang, value);
|
||||
}
|
||||
} else if (node.getNodeType() == Node.TEXT_NODE) {
|
||||
localText.append(node.getTextContent());
|
||||
}
|
||||
}
|
||||
|
||||
// Some descriptions don't list multilingual variants
|
||||
String defaultText = localText.toString().trim();
|
||||
if (defaultText.length() > 0) {
|
||||
texts.put("default", localText.toString());
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take our metadata and inject it into a live Media object.
|
||||
* Media object might contain stale or cached data, or emptiness.
|
||||
* @param media
|
||||
*/
|
||||
public void fill(Media media) {
|
||||
if (!fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch().");
|
||||
}
|
||||
|
||||
media.setCategories(categories);
|
||||
media.setDescriptions(descriptions);
|
||||
media.setCoordinates(coordinates);
|
||||
if (license != null) {
|
||||
media.setLicense(license);
|
||||
}
|
||||
|
||||
// add author, date, etc fields
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.core.text.HtmlCompat
|
||||
import fr.free.nrw.commons.media.IdAndLabels
|
||||
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
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Fetch additional media data from the network that we don't store locally.
|
||||
*
|
||||
*
|
||||
* This includes things like category lists and multilingual descriptions, which are not intrinsic
|
||||
* to the media and may change due to editing.
|
||||
*/
|
||||
@Singleton
|
||||
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) -> IdAndLabels(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
|
||||
fun fetchCreatorIdsAndLabels(media: Media) =
|
||||
mediaClient
|
||||
.getEntities(media.creatorIds)
|
||||
.map {
|
||||
it
|
||||
.entities()
|
||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
|
||||
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 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 getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
|
||||
|
||||
/**
|
||||
* Fetches wikitext from mediaClient
|
||||
*/
|
||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
|
||||
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
|
||||
protected final Media media;
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
|
||||
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
|
||||
this.media = media;
|
||||
this.mediaWikiApi = mwApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
try {
|
||||
return mediaWikiApi.findThumbnailByFilename(params[0]);
|
||||
} catch (Exception e) {
|
||||
// Do something better!
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
106
app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.support.v4.util.LruCache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaWikiImageView extends SimpleDraweeView {
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject LruCache<String, String> thumbnailUrlCache;
|
||||
|
||||
private ThumbnailFetchTask currentThumbnailTask;
|
||||
|
||||
public MediaWikiImageView(Context context) {
|
||||
this(context, null);
|
||||
init();
|
||||
}
|
||||
|
||||
public MediaWikiImageView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
init();
|
||||
}
|
||||
|
||||
public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setMedia(Media media) {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
}
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailUrlCache.get(media.getFilename()) != null) {
|
||||
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
|
||||
currentThumbnailTask.execute(media.getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (currentThumbnailTask != null) {
|
||||
currentThumbnailTask.cancel(true);
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
|
||||
setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_image_black_24dp, getContext().getTheme()))
|
||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_image_black_24dp, getContext().getTheme()))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void setImageUrl(@Nullable String url) {
|
||||
setImageURI(url);
|
||||
}
|
||||
|
||||
private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
|
||||
ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
|
||||
super(media, mwApi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(result) && media.getLocalUri() != null) {
|
||||
result = media.getLocalUri().toString();
|
||||
} else {
|
||||
// only cache meaningful thumbnails received from network.
|
||||
try {
|
||||
thumbnailUrlCache.put(media.getFilename(), result);
|
||||
} catch (NullPointerException npe) {
|
||||
Timber.e("error when adding pic to cache " + npe);
|
||||
|
||||
Toast.makeText(getContext(), R.string.error_while_cache, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
setImageUrl(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.wikidata.GsonUtil
|
||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwErrorResponse
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwIOException
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwLegacyServiceError
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object OkHttpConnectionFactory {
|
||||
private const val CACHE_DIR_NAME = "okhttp-cache"
|
||||
private const val NET_CACHE_SIZE = (64 * 1024 * 1024).toLong()
|
||||
|
||||
@VisibleForTesting
|
||||
var CLIENT: OkHttpClient? = null
|
||||
|
||||
fun getClient(cookieJar: CommonsCookieJar): OkHttpClient {
|
||||
if (CLIENT == null) {
|
||||
CLIENT = createClient(cookieJar)
|
||||
}
|
||||
return CLIENT!!
|
||||
}
|
||||
|
||||
private fun createClient(cookieJar: CommonsCookieJar): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.cookieJar(cookieJar)
|
||||
.cache(
|
||||
if (CommonsApplication.instance != null) Cache(
|
||||
File(CommonsApplication.instance.cacheDir, CACHE_DIR_NAME),
|
||||
NET_CACHE_SIZE
|
||||
) else null
|
||||
)
|
||||
.connectTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
redactHeader("Authorization")
|
||||
redactHeader("Cookie")
|
||||
})
|
||||
.addInterceptor(UnsuccessfulResponseInterceptor())
|
||||
.addInterceptor(CommonHeaderRequestInterceptor())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
class CommonHeaderRequestInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request().newBuilder()
|
||||
.header("User-Agent", CommonsApplication.instance.userAgent)
|
||||
.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log"
|
||||
const val SUPPRESS_ERROR_LOG_HEADER: String = "$SUPPRESS_ERROR_LOG: true"
|
||||
|
||||
private class UnsuccessfulResponseInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val 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.
|
||||
val suppressErrors = rq.headers.names().contains(SUPPRESS_ERROR_LOG)
|
||||
val request = rq.newBuilder()
|
||||
.removeHeader(SUPPRESS_ERROR_LOG)
|
||||
.build()
|
||||
|
||||
val rsp = chain.proceed(request)
|
||||
|
||||
// Do not intercept certain requests and let the caller handle the errors
|
||||
if (isExcludedUrl(chain.request())) {
|
||||
return rsp
|
||||
}
|
||||
if (rsp.isSuccessful) {
|
||||
try {
|
||||
rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody ->
|
||||
if (ERRORS_PREFIX == responseBody.string()) {
|
||||
rsp.body.use { body ->
|
||||
val bodyString = body!!.string()
|
||||
|
||||
throw MwIOException(
|
||||
"MediaWiki API returned error: $bodyString",
|
||||
GsonUtil.defaultGson.fromJson(
|
||||
bodyString,
|
||||
MwErrorResponse::class.java
|
||||
).error!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: MwIOException) {
|
||||
// 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)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
throw IOException("Unsuccessful response")
|
||||
}
|
||||
|
||||
private fun isExcludedUrl(request: Request): Boolean {
|
||||
val requestUrl = request.url.toString()
|
||||
for (url in DO_NOT_INTERCEPT) {
|
||||
if (requestUrl.contains(url)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DO_NOT_INTERCEPT = listOf(
|
||||
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"
|
||||
)
|
||||
const val ERRORS_PREFIX = "{\"error"
|
||||
}
|
||||
}
|
||||
97
app/src/main/java/fr/free/nrw/commons/PageTitle.java
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class PageTitle {
|
||||
private final String namespace;
|
||||
private final String titleKey;
|
||||
|
||||
/**
|
||||
* Construct from a namespace-prefixed page name.
|
||||
* @param prefixedText namespace-prefixed page name
|
||||
*/
|
||||
public PageTitle(@NonNull String prefixedText) {
|
||||
String[] segments = prefixedText.trim().replace(" ", "_").split(":", 2);
|
||||
|
||||
// canonicalize and capitalize page title as done by MediaWiki
|
||||
if (segments.length == 2) {
|
||||
// TODO: canonicalize and capitalize namespace as well
|
||||
// see https://www.mediawiki.org/wiki/Manual:Title.php#Canonical_forms
|
||||
namespace = segments[0];
|
||||
titleKey = Utils.capitalize(segments[1]);
|
||||
} else {
|
||||
namespace = "";
|
||||
titleKey = Utils.capitalize(segments[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonicalized title for displaying (such as "File:My example.jpg").
|
||||
*
|
||||
* @return canonical title
|
||||
*/
|
||||
@NonNull
|
||||
public String getPrefixedText() {
|
||||
if (namespace.isEmpty()) {
|
||||
return titleKey;
|
||||
} else {
|
||||
return namespace + ":" + titleKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonical title for DB and URLs (such as "File:My_example.jpg").
|
||||
*
|
||||
* @return canonical title
|
||||
*/
|
||||
@NonNull
|
||||
public String getDisplayText() {
|
||||
return getPrefixedText().replace("_", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to a URI
|
||||
* (such as "https://commons.wikimedia.org/wiki/File:My_example.jpg").
|
||||
*
|
||||
* @return URI
|
||||
*/
|
||||
@NonNull
|
||||
public Uri getCanonicalUri() {
|
||||
String uriStr = BuildConfig.HOME_URL + Uri.encode(getPrefixedText(), ":/");
|
||||
return Uri.parse(uriStr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert to a mobile URI
|
||||
* (such as "https://commons.m.wikimedia.org/wiki/File:My_example.jpg").
|
||||
*
|
||||
* @return URI
|
||||
*/
|
||||
@NonNull
|
||||
public Uri getMobileUri() {
|
||||
String uriStr = BuildConfig.MOBILE_HOME_URL + Uri.encode(getPrefixedText(), ":/");
|
||||
return Uri.parse(uriStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonical title without namespace.
|
||||
* @return title
|
||||
*/
|
||||
@NonNull
|
||||
public String getText() {
|
||||
return titleKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the canonicalized title for displaying (such as "File:My example.jpg").
|
||||
* </p>
|
||||
* Essentially equivalent to getPrefixedText
|
||||
* @return canonical title as a String
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPrefixedText();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
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 FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
|
||||
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
||||
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
||||
}
|
||||
135
app/src/main/java/fr/free/nrw/commons/Utils.java
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Utils {
|
||||
|
||||
/**
|
||||
* Strips localization symbols from a string.
|
||||
* Removes the suffix after "@" and quotes.
|
||||
*
|
||||
* @param s string possibly containing localization symbols
|
||||
* @return stripped string
|
||||
*/
|
||||
public static String stripLocalizedString(String s) {
|
||||
Matcher matcher = Pattern.compile("\\\"(.*)\\\"(@\\w+)?").matcher(s);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
public static String makeThumbBaseUrl(@NonNull String filename) {
|
||||
String name = new PageTitle(filename).getPrefixedText();
|
||||
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
|
||||
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
|
||||
}
|
||||
|
||||
public static String urlEncode(String url) {
|
||||
try {
|
||||
return URLEncoder.encode(url, "utf-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalizes the first character of a string.
|
||||
*
|
||||
* @param string
|
||||
* @return string with capitalized first character
|
||||
*/
|
||||
public static String capitalize(String string) {
|
||||
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
|
||||
}
|
||||
|
||||
public static int licenseNameFor(String license) {
|
||||
switch (license) {
|
||||
case Prefs.Licenses.CC_BY_3:
|
||||
return R.string.license_name_cc_by;
|
||||
case Prefs.Licenses.CC_BY_4:
|
||||
return R.string.license_name_cc_by_four;
|
||||
case Prefs.Licenses.CC_BY_SA_3:
|
||||
return R.string.license_name_cc_by_sa;
|
||||
case Prefs.Licenses.CC_BY_SA_4:
|
||||
return R.string.license_name_cc_by_sa_four;
|
||||
case Prefs.Licenses.CC0:
|
||||
return R.string.license_name_cc0;
|
||||
case Prefs.Licenses.CC_BY: // for backward compatibility to v2.1
|
||||
return R.string.license_name_cc_by_3_0;
|
||||
case Prefs.Licenses.CC_BY_SA: // for backward compatibility to v2.1
|
||||
return R.string.license_name_cc_by_sa_3_0;
|
||||
}
|
||||
throw new RuntimeException("Unrecognized license value: " + license);
|
||||
}
|
||||
|
||||
public static String fixExtension(String title, String extension) {
|
||||
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// People are used to ".jpg" more than ".jpeg" which the system gives us.
|
||||
if (extension != null && extension.toLowerCase(Locale.ENGLISH).equals("jpeg")) {
|
||||
extension = "jpg";
|
||||
}
|
||||
title = jpegPattern.matcher(title).replaceFirst(".jpg");
|
||||
if (extension != null && !title.toLowerCase(Locale.getDefault())
|
||||
.endsWith("." + extension.toLowerCase(Locale.ENGLISH))) {
|
||||
title += "." + extension;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be used to fetch the logs generated by the app ever since the beginning of times....
|
||||
* i.e. since the time the app started.
|
||||
*
|
||||
* @return String containing all the logs since the time the app started
|
||||
*/
|
||||
public static String getAppLogs() {
|
||||
final String processId = Integer.toString(android.os.Process.myPid());
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
try {
|
||||
String[] command = new String[] {"logcat","-d","-v","threadtime"};
|
||||
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
|
||||
BufferedReader bufferedReader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream())
|
||||
);
|
||||
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (line.contains(processId)) {
|
||||
stringBuilder.append(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Timber.e("getAppLogs failed", ioe);
|
||||
}
|
||||
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
7
app/src/main/java/fr/free/nrw/commons/ViewHolder.java
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public interface ViewHolder<T> {
|
||||
void bindModel(Context context, T model);
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* This adapter will be used to display fragments in a ViewPager
|
||||
*/
|
||||
class ViewPagerAdapter : FragmentPagerAdapter {
|
||||
private val context: Context
|
||||
private var fragmentList: List<Fragment> = emptyList()
|
||||
private var fragmentTitleList: List<String> = emptyList()
|
||||
|
||||
constructor(context: Context, manager: FragmentManager) : super(manager) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
constructor(context: Context, manager: FragmentManager, behavior: Int) : super(manager, behavior) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment = fragmentList[position]
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence = fragmentTitleList[position]
|
||||
|
||||
override fun getCount(): Int = fragmentList.size
|
||||
|
||||
fun setTabs(vararg titlesToFragments: Pair<Int, Fragment>) {
|
||||
// Enforce that every title must come from strings.xml and all will consistently be uppercase
|
||||
fragmentTitleList = titlesToFragments.map {
|
||||
context.getString(it.first).uppercase(Locale.ROOT)
|
||||
}
|
||||
fragmentList = titlesToFragments.map { it.second }
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Convenience method for Java callers, can be removed when everything is migrated
|
||||
@JvmStatic
|
||||
fun pairOf(first: Int, second: Fragment) = first to second
|
||||
}
|
||||
}
|
||||
43
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.ViewPager;
|
||||
|
||||
import com.viewpagerindicator.CirclePageIndicator;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
|
||||
public class WelcomeActivity extends BaseActivity {
|
||||
|
||||
@BindView(R.id.welcomePager) ViewPager pager;
|
||||
@BindView(R.id.welcomePagerIndicator) CirclePageIndicator indicator;
|
||||
|
||||
private WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_welcome);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
pager.setAdapter(adapter);
|
||||
indicator.setViewPager(pager);
|
||||
adapter.setCallback(this::finish);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
adapter.setCallback(null);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public static void startYourself(Context context) {
|
||||
Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
||||
context.startActivity(welcomeIntent);
|
||||
}
|
||||
}
|
||||