With data-client added as library module (#3656)

* With data-client added as library module

* Fix build
This commit is contained in:
Vivek Maskara 2020-04-15 03:00:13 -07:00 committed by GitHub
parent 9ee04f3df4
commit 32ee0b4f9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
258 changed files with 34820 additions and 2 deletions

59
data-client/.gitignore vendored Normal file
View file

@ -0,0 +1,59 @@
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

23
data-client/.travis.yml Normal file
View file

@ -0,0 +1,23 @@
language: android
jdk: oraclejdk8
android:
components:
- platform-tools
- tools
# The BuildTools version used by your project
- build-tools-28.0.3
# The SDK version used to compile your project
- android-28
# Additional components
- extra-android-m2repository
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+
before_script:
- chmod +x gradlew
script: "./gradlew test"

201
data-client/LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

101
data-client/README.md Normal file
View file

@ -0,0 +1,101 @@
# Wikimedia Android data client
An Android library for communicating with Wikimedia projects, with Rx bindings and other utilities.
## Motivation and philosophy
Here are the purposes for creating this library:
* Encapsulate the various model structures that are returned by MediaWiki APIs,
as well as by REST APIs provided by Wikimedia services.
* Provide high-level bindings for Retrofit and RxJava for executing calls to MediaWiki APIs to
further simplify client integration, while also allowing customization and extension.
* Provide numerous common utility methods, so that they don't need to be duplicated.
## Integration with your app
Add the dependency to your Gradle file as usual:
```
implementation "com.dmitrybrant:wikimedia-android-data-client:0.0.18"
```
The only nontrivial point of integration with the library is the `AppAdapter` class: You
need to create a class that inherits from `AppAdapter` and implement its methods. The
methods are mostly self-explanatory, and deal with user account management, cookie storage,
and a few other customizations.
Once you create this class (suppose it's called `MyAppAdapter`), you should pass it into
the `AppAdapter` singleton when your app starts:
```
@Override
public void onCreate() {
...
AppAdapter.set(new MyAppAdapter());
...
}
```
## Making calls to APIs
Notice that there is an interface called `Service` that contains a number of API definitions
for talking with a MediaWiki server. To use any of the functions in the interface, you should
use the `ServiceFactory` class. For example:
```
WikiSite wiki = new WikiSite("en.wikipedia.org");
Observable observable = ServiceFactory.get(wiki).fullTextSearch("foo");
```
That's it! Notice that most of the API calls return an `Observable` response which you can
feed into an Rx subscription.
Note: the `ServiceFactory` class contains automatic caching logic, so that multiple calls to
`get()` the service for the same `WikiSite` will be very efficient.
## Custom API calls
The `ServiceFactory` class also allows you to provide a service interface with custom
API functions. Suppose you create your own service interface that looks like this:
```
public interface MyInterface {
@GET("action=myawesomeaction")
Observable<MyAwesomeResponse> myAwesomeApiCall(@Query("parameter1") parameter);
}
```
You can then use it with `ServiceFactory` this way:
```
WikiSite wiki = new WikiSite("my.awesome.wiki");
Observable observable = ServiceFactory.get(wiki, "https://my.awesome.wiki/", MyInterface.class)
.myAwesomeApiCall("foo");
```
## Utility methods
The library contains a potpourri of utility methods found under the `util` package. Feel free
to browse through them and use them as necessary.
## License
Copyright 2019 Wikimedia Foundation
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.

147
data-client/build.gradle Normal file
View file

@ -0,0 +1,147 @@
buildscript {
ext.kotlin_version = '1.3.31'
repositories {
jcenter()
google()
}
dependencies {
classpath "com.android.tools.build:gradle:3.4.1"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id "com.jfrog.bintray" version "1.7.3"
}
allprojects {
repositories {
google()
jcenter()
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
}
version = "${VERSION_NAME}"
group = "${GROUP_ID}"
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "${VERSION_NAME}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
}
dependencies {
String retrofitVersion = '2.4.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.core:core:1.0.2"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation "io.reactivex.rxjava2:rxjava:2.2.3"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.robolectric:robolectric:3.8'
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.1"
testImplementation "commons-io:commons-io:2.6"
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
artifacts {
archives sourcesJar
}
Properties properties = new Properties()
if ( project.rootProject.file('local.properties').isFile() ) {
properties.load(project.rootProject.file('local.properties').newDataInputStream())
}
bintray {
user = properties.getProperty("bintray.user")
key = properties.getProperty("bintray.apikey")
println 'Bintray user: ' + user
configurations = ['archives']
pkg {
repo = 'maven'
name = "${ARTIFACT_ID}"
vcsUrl = 'https://github.com/wikimedia/wikimedia-android-data-client.git'
licenses = ['Apache-2.0']
version {
name = "${VERSION_NAME}"
}
publish = true
}
}
install {
repositories.mavenInstaller {
// This generates POM.xml with proper parameters
pom {
project {
packaging 'aar'
name "${ARTIFACT_ID}"
artifactId "${ARTIFACT_ID}"
description 'Android library for accessing the Wikimedia APIs.'
url 'https://github.com/wikimedia/wikimedia-android-data-client'
inceptionYear '2019'
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id 'dmitrybrant'
name 'Dmitry Brant'
email 'me@dmitrybrant.com'
}
}
}
}
}
}
repositories {
mavenCentral()
}

View file

@ -0,0 +1,25 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=0.0.27
GROUP_ID=com.dmitrybrant
ARTIFACT_ID=wikimedia-android-data-client
GRADLE_BINTRAY_PLUGIN_VERSION=1.7.3
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Fri Jun 07 09:14:55 EDT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

160
data-client/gradlew vendored Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
data-client/gradlew.bat vendored Normal file
View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

25
data-client/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in F:\android-sdk-windows/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,101 @@
#!/usr/bin/env python
# coding=utf-8
from datetime import datetime, timedelta
import lxml
import lxml.builder as lb
import json
import requests
QUERY_SITEMATRIX = 'https://www.mediawiki.org/w/api.php?action=sitematrix' \
'&format=json&formatversion=2&smtype=language&smstate=all'
QUERY_ALLUSERS = '/w/api.php?action=query&format=json&formatversion=2&list=allusers' \
'&aulimit=50&auactiveusers=1&auwitheditsonly=1'
lang_keys = []
lang_local_names = []
lang_eng_names = []
lang_rank = []
def add_lang(key, local_name, eng_name, rank):
rank_pos = 0
# Automatically keep the arrays sorted by rank
for index, item in enumerate(lang_rank):
rank_pos = index
if (rank > item):
break
lang_keys.insert(rank_pos, key)
lang_local_names.insert(rank_pos, local_name)
lang_eng_names.insert(rank_pos, eng_name)
lang_rank.insert(rank_pos, rank)
data = json.loads(requests.get(QUERY_SITEMATRIX).text)
for key, value in data[u"sitematrix"].items():
if type(value) is not dict:
continue
language_code = value[u"code"]
if language_code == 'got':
# 'got' is Gothic Runes, which lie outside the Basic Multilingual Plane
# Android segfaults on these. So let's ignore those.
continue
site_list = value[u"site"]
if type(site_list) is not list:
continue
wikipedia_url = ""
for site in site_list:
if "wikipedia.org" in site[u"url"] and u"closed" not in site:
wikipedia_url = site[u"url"]
if len(wikipedia_url) == 0:
continue
# TODO: If we want to remove languages with too few active users:
# allusers = json.loads(requests.get(wikipedia_url + QUERY_ALLUSERS).text)
# if len(allusers[u"query"][u"allusers"]) < 10:
# print ("Excluding " + language_code + " (too few active users).")
# continue
# Use the AQS API to get total pageviews for this language wiki in the last month:
date = datetime.today() - timedelta(days=31)
unique_device_response = json.loads(requests.get('https://wikimedia.org/api/rest_v1/metrics/unique-devices/' +
wikipedia_url.replace('https://', '') + '/all-sites/monthly/' +
date.strftime('%Y%m01') + '/' + date.strftime('%Y%m01')).text)
rank = 0
if u"items" in unique_device_response:
if len(unique_device_response[u"items"]) > 0:
rank = unique_device_response[u"items"][0][u"devices"]
print ("Rank for " + language_code + ": " + str(rank))
if language_code == 'zh':
add_lang(key='zh-hans', local_name=u'简体中文',
eng_name='Simplified Chinese', rank=rank)
add_lang(key='zh-hant', local_name=u'繁體中文',
eng_name='Traditional Chinese', rank=rank)
continue
if language_code == 'no': # T114042
language_code = 'nb'
add_lang(language_code, value[u"name"].replace("'", "\\'"), value[u"localname"].replace("'", "\\'"), rank)
add_lang(key='test', local_name='Test', eng_name='Test', rank=0)
add_lang(key='en-x-piglatin', local_name='Igpay Atinlay', eng_name='Pig Latin', rank=0)
# Generate the XML, for Android
NAMESPACE = 'http://schemas.android.com/tools'
TOOLS = '{%s}' % NAMESPACE
x = lb.ElementMaker(nsmap={'tools': NAMESPACE})
keys = [x.item(k) for k in lang_keys]
local_names = [x.item(k) for k in lang_local_names]
eng_names = [x.item(k) for k in lang_eng_names]
resources = x.resources(
getattr(x, 'string-array')(*keys, name='preference_language_keys'),
getattr(x, 'string-array')(*local_names, name='preference_language_local_names'),
getattr(x, 'string-array')(*eng_names, name='preference_language_canonical_names'))
resources.set(TOOLS + 'ignore', 'MissingTranslation')
with open('../src/main/res/values/languages_list.xml', 'wb') as f:
f.write(lxml.etree.tostring(resources, pretty_print=True,
xml_declaration=True, encoding='utf-8'))

View file

@ -0,0 +1,183 @@
#!/usr/bin/env python
# coding=utf-8
import copy
import os
import json
import codecs
import requests
from jinja2 import Environment, FileSystemLoader
CHINESE_WIKI_LANG = "zh"
SIMPLIFIED_CHINESE_LANG = "zh-hans"
TRADITIONAL_CHINESE_LANG = "zh-hant"
# T114042
NORWEGIAN_BOKMAL_WIKI_LANG = "no"
NORWEGIAN_BOKMAL_LANG = "nb"
# Wikis that cause problems and hence we pretend
# do not exist.
# - "got" -> Gothic runes wiki. The name of got in got
# contains characters outside the Unicode BMP. Android
# hard crashes on these. Let's ignore these fellas
# for now.
# - "mo" -> Moldovan, which automatically redirects to Romanian (ro),
# which already exists in our list.
OSTRICH_WIKIS = [u"got", "mo"]
# Represents a single wiki, along with arbitrary properties of that wiki
# Simple data container object
class Wiki(object):
def __init__(self, lang):
self.lang = lang
self.props = {}
# Represents a list of wikis plus their properties.
# Encapsulates rendering code as well
class WikiList(object):
def __init__(self, wikis):
self.wikis = wikis
self.template_env = Environment(loader=FileSystemLoader(
os.path.join(os.path.dirname(os.path.realpath(__file__)), u"templates")
))
def render(self, template, class_name, **kwargs):
data = {
u"class_name": class_name,
u"wikis": self.wikis
}
data.update(kwargs)
rendered = self.template_env.get_template(template).render(**data)
out = codecs.open(u"../src/main/java/org/wikipedia/staticdata/" + class_name + u".java", u"w", u"utf-8")
out.write(rendered)
out.close()
def build_wiki(lang, english_name, local_name):
wiki = Wiki(lang)
wiki.props["english_name"] = english_name
wiki.props["local_name"] = local_name
return wiki
def list_from_sitematrix():
QUERY_SITEMATRIX = 'https://www.mediawiki.org/w/api.php?action=sitematrix' \
'&format=json&formatversion=2&smtype=language&smstate=all'
print(u"Fetching languages...")
data = json.loads(requests.get(QUERY_SITEMATRIX).text)
wikis = []
for key, value in data[u"sitematrix"].items():
if type(value) is not dict:
continue
site_list = value[u"site"]
if type(site_list) is not list:
continue
wikipedia_url = ""
for site in site_list:
if "wikipedia.org" in site[u"url"] and u"closed" not in site:
wikipedia_url = site[u"url"]
if len(wikipedia_url) == 0:
continue
wikis.append(build_wiki(value[u"code"], value[u"localname"], value[u"name"]))
return wikis
# Remove unsupported wikis.
def filter_supported_wikis(wikis):
return [wiki for wiki in wikis if wiki.lang not in OSTRICH_WIKIS]
# Apply manual tweaks to the list of wikis before they're populated.
def preprocess_wikis(wikis):
# Add TestWiki.
wikis.append(build_wiki(lang="test", english_name="Test", local_name="Test"))
return wikis
# Apply manual tweaks to the list of wikis after they're populated.
def postprocess_wikis(wiki_list):
# Add Simplified and Traditional Chinese dialects.
chineseWiki = next((wiki for wiki in wiki_list.wikis if wiki.lang == CHINESE_WIKI_LANG), None)
chineseWikiIndex = wiki_list.wikis.index(chineseWiki)
simplifiedWiki = copy.deepcopy(chineseWiki)
simplifiedWiki.lang = SIMPLIFIED_CHINESE_LANG
simplifiedWiki.props["english_name"] = "Simplified Chinese"
simplifiedWiki.props["local_name"] = "简体中文"
wiki_list.wikis.insert(chineseWikiIndex + 1, simplifiedWiki)
traditionalWiki = copy.deepcopy(chineseWiki)
traditionalWiki.lang = TRADITIONAL_CHINESE_LANG
traditionalWiki.props["english_name"] = "Traditional Chinese"
traditionalWiki.props["local_name"] = "繁體中文"
wiki_list.wikis.insert(chineseWikiIndex + 2, traditionalWiki)
bokmalWiki = next((wiki for wiki in wiki_list.wikis if wiki.lang == NORWEGIAN_BOKMAL_WIKI_LANG), None)
bokmalWiki.lang = NORWEGIAN_BOKMAL_LANG
return wiki_list
# Populate the aliases for "Special:" and "File:" in all wikis
def populate_aliases(wikis):
for wiki in wikis.wikis:
print(u"Fetching Special Page and File alias for %s" % wiki.lang)
url = u"https://%s.wikipedia.org/w/api.php" % wiki.lang + \
u"?action=query&meta=siteinfo&format=json&siprop=namespaces"
data = json.loads(requests.get(url).text)
# according to https://www.mediawiki.org/wiki/Manual:Namespace
# -1 seems to be the ID for Special Pages
wiki.props[u"special_alias"] = data[u"query"][u"namespaces"][u"-1"][u"*"]
# 6 is the ID for File pages
wiki.props[u"file_alias"] = data[u"query"][u"namespaces"][u"6"][u"*"]
return wikis
# Populates data on names of main page in each wiki
def populate_main_pages(wikis):
for wiki in wikis.wikis:
print(u"Fetching Main Page for %s" % wiki.lang)
url = u"https://%s.wikipedia.org/w/api.php" % wiki.lang + \
u"?action=query&meta=siteinfo&format=json&siprop=general"
data = json.loads(requests.get(url).text)
wiki.props[u"main_page_name"] = data[u"query"][u"general"][u"mainpage"]
return wikis
# Returns a function that renders a particular template when passed
# a WikiList object
def render_template(template, filename, **kwargs):
def _actual_render(wikis):
wikis.render(template, filename, **kwargs)
return wikis
return _actual_render
# Kinda like reduce(), but special cases first function
def chain(*funcs):
res = funcs[0]()
for func in funcs[1:]:
res = func(res)
chain(
list_from_sitematrix,
filter_supported_wikis,
preprocess_wikis,
WikiList,
populate_aliases,
populate_main_pages,
postprocess_wikis,
render_template(u"basichash.java.jinja", u"SpecialAliasData", key=u"special_alias"),
render_template(u"basichash.java.jinja", u"FileAliasData", key=u"file_alias"),
render_template(u"basichash.java.jinja", u"MainPageNameData", key=u"main_page_name"),
)

View file

@ -0,0 +1,36 @@
/*
This file is auto-generated from a template (/scripts/templates).
If you need to modify it, make sure to modify the template, not this file.
*/
package org.wikipedia.staticdata;
import android.support.annotation.NonNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public final class {{class_name}} {
@NonNull private static final Map<String, String> DATA_MAP = Collections.unmodifiableMap(newMap());
@NonNull public static String valueFor(String key) {
if (DATA_MAP.containsKey(key)) {
return DATA_MAP.get(key);
}
return DATA_MAP.get("en");
}
@SuppressWarnings({"checkstyle:methodlength", "SpellCheckingInspection"})
private static Map<String, String> newMap() {
final int size = {{wikis|length}};
Map<String, String> map = new HashMap<>(size);
{%- for wiki in wikis %}
map.put("{{wiki.lang}}", "{{wiki.props[key]}}");
{%- endfor %}
return map;
}
private {{class_name}}() { }
}

View file

@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.wikipedia.dataclient">
<application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true">
</application>
</manifest>

View file

@ -0,0 +1,38 @@
package org.wikipedia;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.login.LoginResult;
import okhttp3.OkHttpClient;
public abstract class AppAdapter {
public abstract String getMediaWikiBaseUrl();
public abstract String getRestbaseUriFormat();
public abstract OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite);
public abstract int getDesiredLeadImageDp();
public abstract boolean isLoggedIn();
public abstract String getUserName();
public abstract String getPassword();
public abstract void updateAccount(@NonNull LoginResult result);
public abstract SharedPreferenceCookieManager getCookies();
public abstract void setCookies(@NonNull SharedPreferenceCookieManager cookies);
public abstract boolean logErrorsInsteadOfCrashing();
private static AppAdapter INSTANCE;
public static void set(AppAdapter instance) {
INSTANCE = instance;
}
public static AppAdapter get() {
if (INSTANCE == null) {
throw new RuntimeException("Please provide an instance of AppAdapter when using this library.");
}
return INSTANCE;
}
}

View file

@ -0,0 +1,19 @@
package org.wikipedia.captcha;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwResponse;
public class Captcha extends MwResponse {
@SuppressWarnings("unused,NullableProblems") @NonNull private FancyCaptchaReload fancycaptchareload;
@NonNull String captchaId() {
return fancycaptchareload.index();
}
private static class FancyCaptchaReload {
@SuppressWarnings("unused,NullableProblems") @NonNull private String index;
@NonNull String index() {
return index;
}
}
}

View file

@ -0,0 +1,49 @@
package org.wikipedia.captcha;
import android.os.Parcel;
import android.os.Parcelable;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.edit.EditResult;
// Handles only Image Captchas
public class CaptchaResult extends EditResult {
private final String captchaId;
public CaptchaResult(String captchaId) {
super("Failure");
this.captchaId = captchaId;
}
protected CaptchaResult(Parcel in) {
super(in);
captchaId = in.readString();
}
public String getCaptchaId() {
return captchaId;
}
public String getCaptchaUrl(WikiSite wiki) {
return wiki.url("index.php") + "?title=Special:Captcha/image&wpCaptchaId=" + captchaId;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(captchaId);
}
public static final Parcelable.Creator<CaptchaResult> CREATOR
= new Parcelable.Creator<CaptchaResult>() {
@Override
public CaptchaResult createFromParcel(Parcel in) {
return new CaptchaResult(in);
}
@Override
public CaptchaResult[] newArray(int size) {
return new CaptchaResult[size];
}
};
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.concurrency;
import androidx.annotation.NonNull;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.CheckReturnValue;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
public class RxBus {
private final Subject<Object> bus = PublishSubject.create().toSerialized();
private final Observable<Object> observable = bus.observeOn(AndroidSchedulers.mainThread());
public void post(Object o) {
bus.onNext(o);
}
@CheckReturnValue
public Disposable subscribe(@NonNull Consumer<Object> consumer) {
return observable.subscribe(consumer);
}
}

View file

@ -0,0 +1,12 @@
package org.wikipedia.createaccount;
import androidx.annotation.NonNull;
/**
* Exception thrown when an account creation request FAILs
*/
public class CreateAccountException extends RuntimeException {
CreateAccountException(@NonNull String message) {
super(message);
}
}

View file

@ -0,0 +1,57 @@
package org.wikipedia.createaccount;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CreateAccountResult implements Parcelable {
@NonNull private final String status;
@NonNull private final String message;
public CreateAccountResult(@NonNull String status, @NonNull String message) {
this.status = status;
this.message = message;
}
@NonNull
public String getStatus() {
return status;
}
@NonNull
public String getMessage() {
return message;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(status);
parcel.writeString(message);
}
@Override
public int describeContents() {
return 0;
}
protected CreateAccountResult(Parcel in) {
status = in.readString();
message = in.readString();
}
@NonNull
public static final Parcelable.Creator<CreateAccountResult> CREATOR
= new Parcelable.Creator<CreateAccountResult>() {
@Override
public CreateAccountResult createFromParcel(Parcel in) {
return new CreateAccountResult(in);
}
@Override
public CreateAccountResult[] newArray(int size) {
return new CreateAccountResult[size];
}
};
}

View file

@ -0,0 +1,43 @@
package org.wikipedia.createaccount;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
class CreateAccountSuccessResult extends CreateAccountResult implements Parcelable {
private String username;
CreateAccountSuccessResult(@NonNull String username) {
super("PASS", "Account created");
this.username = username;
}
String getUsername() {
return username;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeString(username);
}
private CreateAccountSuccessResult(Parcel in) {
super(in);
username = in.readString();
}
public static final Creator<CreateAccountSuccessResult> CREATOR
= new Creator<CreateAccountSuccessResult>() {
@Override
public CreateAccountSuccessResult createFromParcel(Parcel in) {
return new CreateAccountSuccessResult(in);
}
@Override
public CreateAccountSuccessResult[] newArray(int size) {
return new CreateAccountSuccessResult[size];
}
};
}

View file

@ -0,0 +1,213 @@
package org.wikipedia.csrf;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.AppAdapter;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.login.LoginClient;
import org.wikipedia.login.LoginResult;
import org.wikipedia.util.log.L;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Response;
public class CsrfTokenClient {
private static final String ANON_TOKEN = "+\\";
private static final int MAX_RETRIES = 1;
private static final int MAX_RETRIES_OF_LOGIN_BLOCKING = 2;
@NonNull private final WikiSite csrfWikiSite;
@NonNull private final WikiSite loginWikiSite;
private int retries = 0;
@Nullable private Call<MwQueryResponse> csrfTokenCall;
@NonNull private LoginClient loginClient = new LoginClient();
public CsrfTokenClient(@NonNull WikiSite csrfWikiSite, @NonNull WikiSite loginWikiSite) {
this.csrfWikiSite = csrfWikiSite;
this.loginWikiSite = loginWikiSite;
}
public void request(@NonNull final Callback callback) {
request(false, callback);
}
public void request(boolean forceLogin, @NonNull final Callback callback) {
cancel();
if (forceLogin) {
retryWithLogin(new RuntimeException("Forcing login..."), callback);
return;
}
csrfTokenCall = request(ServiceFactory.get(csrfWikiSite), callback);
}
public void cancel() {
loginClient.cancel();
if (csrfTokenCall != null) {
csrfTokenCall.cancel();
csrfTokenCall = null;
}
}
@VisibleForTesting
@NonNull
Call<MwQueryResponse> request(@NonNull Service service, @NonNull final Callback cb) {
return requestToken(service, new CsrfTokenClient.Callback() {
@Override public void success(@NonNull String token) {
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
retryWithLogin(new RuntimeException("App believes we're logged in, but got anonymous token."), cb);
} else {
cb.success(token);
}
}
@Override public void failure(@NonNull Throwable caught) {
retryWithLogin(caught, cb);
}
@Override
public void twoFactorPrompt() {
cb.twoFactorPrompt();
}
});
}
private void retryWithLogin(@NonNull Throwable caught, @NonNull final Callback callback) {
if (retries < MAX_RETRIES
&& !TextUtils.isEmpty(AppAdapter.get().getUserName())
&& !TextUtils.isEmpty(AppAdapter.get().getPassword())) {
retries++;
SharedPreferenceCookieManager.getInstance().clearAllCookies();
login(AppAdapter.get().getUserName(), AppAdapter.get().getPassword(), () -> {
L.i("retrying...");
request(callback);
}, callback);
} else {
callback.failure(caught);
}
}
private void login(@NonNull final String username, @NonNull final String password,
@NonNull final RetryCallback retryCallback,
@NonNull final Callback callback) {
new LoginClient().request(loginWikiSite, username, password,
new LoginClient.LoginCallback() {
@Override
public void success(@NonNull LoginResult loginResult) {
if (loginResult.pass()) {
AppAdapter.get().updateAccount(loginResult);
retryCallback.retry();
} else {
callback.failure(new LoginClient.LoginFailedException(loginResult.getMessage()));
}
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
callback.twoFactorPrompt();
}
@Override public void passwordResetPrompt(@Nullable String token) {
// Should not happen here, but call the callback just in case.
callback.failure(new LoginClient.LoginFailedException("Logged in with temporary password."));
}
@Override
public void error(@NonNull Throwable caught) {
callback.failure(caught);
}
});
}
@NonNull public String getTokenBlocking() throws Throwable {
String token = "";
Service service = ServiceFactory.get(csrfWikiSite);
for (int retry = 0; retry < MAX_RETRIES_OF_LOGIN_BLOCKING; retry++) {
try {
if (retry > 0) {
// Log in explicitly
new LoginClient().loginBlocking(loginWikiSite, AppAdapter.get().getUserName(),
AppAdapter.get().getPassword(), "");
}
Response<MwQueryResponse> response = service.getCsrfTokenCall().execute();
if (response.body() == null || response.body().query() == null
|| TextUtils.isEmpty(response.body().query().csrfToken())) {
continue;
}
token = response.body().query().csrfToken();
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
throw new RuntimeException("App believes we're logged in, but got anonymous token.");
}
break;
} catch (Throwable t) {
L.w(t);
}
}
if (TextUtils.isEmpty(token) || token.equals(ANON_TOKEN)) {
throw new IOException("Invalid token, or login failure.");
}
return token;
}
@VisibleForTesting @NonNull Call<MwQueryResponse> requestToken(@NonNull Service service,
@NonNull final Callback cb) {
Call<MwQueryResponse> call = service.getCsrfTokenCall();
call.enqueue(new retrofit2.Callback<MwQueryResponse>() {
@Override
public void onResponse(@NonNull Call<MwQueryResponse> call, @NonNull Response<MwQueryResponse> response) {
if (call.isCanceled()) {
return;
}
cb.success(response.body().query().csrfToken());
}
@Override
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable t) {
if (call.isCanceled()) {
return;
}
cb.failure(t);
}
});
return call;
}
public interface Callback {
void success(@NonNull String token);
void failure(@NonNull Throwable caught);
void twoFactorPrompt();
}
public static class DefaultCallback implements Callback {
@Override
public void success(@NonNull String token) {
}
@Override
public void failure(@NonNull Throwable caught) {
L.e(caught);
}
@Override
public void twoFactorPrompt() {
// TODO:
}
}
private interface RetryCallback {
void retry();
}
}

View file

@ -0,0 +1,194 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.restbase.RbDefinition;
import org.wikipedia.dataclient.restbase.RbRelatedPages;
import org.wikipedia.dataclient.restbase.page.RbPageLead;
import org.wikipedia.dataclient.restbase.page.RbPageRemaining;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.feed.aggregated.AggregatedFeedContent;
import org.wikipedia.feed.announcement.AnnouncementList;
import org.wikipedia.feed.configure.FeedAvailability;
import org.wikipedia.feed.onthisday.OnThisDay;
import org.wikipedia.gallery.Gallery;
import org.wikipedia.readinglist.sync.SyncedReadingLists;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface RestService {
String REST_API_PREFIX = "api/rest_v1/";
String ACCEPT_HEADER_PREFIX = "accept: application/json; charset=utf-8; profile=\"https://www.mediawiki.org/wiki/Specs/";
String ACCEPT_HEADER_SUMMARY = ACCEPT_HEADER_PREFIX + "Summary/1.2.0\"";
String ACCEPT_HEADER_MOBILE_SECTIONS = ACCEPT_HEADER_PREFIX + "mobile-sections/0.12.4\"";
String ACCEPT_HEADER_DEFINITION = ACCEPT_HEADER_PREFIX + "definition/0.7.2\"";
String REST_PAGE_SECTIONS_URL = "page/mobile-sections-remaining/{title}";
/**
* Gets a page summary for a given title -- for link previews
*
* @param title the page title to be used including prefix
*/
@Headers({
"x-analytics: preview=1",
ACCEPT_HEADER_SUMMARY
})
@GET("page/summary/{title}")
@NonNull
Observable<RbPageSummary> getSummary(@Nullable @Header("Referer") String referrerUrl,
@NonNull @Path("title") String title);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
*/
@Headers({
"x-analytics: pageview=1",
ACCEPT_HEADER_MOBILE_SECTIONS
})
@GET("page/mobile-sections-lead/{title}")
@NonNull
Observable<Response<RbPageLead>> getLeadSection(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@Nullable @Header("Referer") String referrerUrl,
@NonNull @Path("title") String title);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@Headers(ACCEPT_HEADER_MOBILE_SECTIONS)
@GET(REST_PAGE_SECTIONS_URL)
@NonNull Observable<Response<RbPageRemaining>> getRemainingSections(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Path("title") String title);
/**
* TODO: remove this if we find a way to get the request url before the observable object being executed
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@Headers(ACCEPT_HEADER_MOBILE_SECTIONS)
@GET(REST_PAGE_SECTIONS_URL)
@NonNull Call<RbPageRemaining> getRemainingSectionsUrl(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Path("title") String title);
// todo: this Content Service-only endpoint is under page/ but that implementation detail should
// probably not be reflected here. Move to WordDefinitionClient
/**
* Gets selected Wiktionary content for a given title derived from user-selected text
*
* @param title the Wiktionary page title derived from user-selected Wikipedia article text
*/
@Headers(ACCEPT_HEADER_DEFINITION)
@GET("page/definition/{title}")
@NonNull Observable<Map<String, RbDefinition.Usage[]>> getDefinition(@NonNull @Path("title") String title);
@Headers(ACCEPT_HEADER_SUMMARY)
@GET("page/random/summary")
@NonNull Observable<RbPageSummary> getRandomSummary();
@Headers(ACCEPT_HEADER_SUMMARY)
@GET("page/related/{title}")
@NonNull Observable<RbRelatedPages> getRelatedPages(@Path("title") String title);
@GET("page/media/{title}")
@NonNull Observable<Gallery> getMedia(@Path("title") String title);
@GET("feed/onthisday/events/{mm}/{dd}")
@NonNull Observable<OnThisDay> getOnThisDay(@Path("mm") int month, @Path("dd") int day);
@Headers(ACCEPT_HEADER_PREFIX + "announcements/0.1.0\"")
@GET("feed/announcements")
@NonNull Call<AnnouncementList> getAnnouncements();
@Headers(ACCEPT_HEADER_PREFIX + "aggregated-feed/0.5.0\"")
@GET("feed/featured/{year}/{month}/{day}")
@NonNull Observable<AggregatedFeedContent> getAggregatedFeed(@Path("year") String year,
@Path("month") String month,
@Path("day") String day);
@GET("feed/availability")
@NonNull Observable<FeedAvailability> getFeedAvailability();
// ------- Reading lists -------
@Headers("Cache-Control: no-cache")
@POST("data/lists/setup")
@NonNull Call<Void> setupReadingLists(@Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@POST("data/lists/teardown")
@NonNull Call<Void> tearDownReadingLists(@Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@GET("data/lists/")
@NonNull Call<SyncedReadingLists> getReadingLists(@Query("next") String next);
@Headers("Cache-Control: no-cache")
@POST("data/lists/")
@NonNull Call<SyncedReadingLists.RemoteIdResponse> createReadingList(@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingList list);
@Headers("Cache-Control: no-cache")
@PUT("data/lists/{id}")
@NonNull Call<Void> updateReadingList(@Path("id") long listId, @Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingList list);
@Headers("Cache-Control: no-cache")
@DELETE("data/lists/{id}")
@NonNull Call<Void> deleteReadingList(@Path("id") long listId, @Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@GET("data/lists/changes/since/{date}")
@NonNull Call<SyncedReadingLists> getReadingListChangesSince(@Path("date") String iso8601Date,
@Query("next") String next);
@Headers("Cache-Control: no-cache")
@GET("data/lists/pages/{project}/{title}")
@NonNull Call<SyncedReadingLists> getReadingListsContaining(@Path("project") String project,
@Path("title") String title,
@Query("next") String next);
@Headers("Cache-Control: no-cache")
@GET("data/lists/{id}/entries/")
@NonNull Call<SyncedReadingLists> getReadingListEntries(@Path("id") long listId, @Query("next") String next);
@Headers("Cache-Control: no-cache")
@POST("data/lists/{id}/entries/")
@NonNull Call<SyncedReadingLists.RemoteIdResponse> addEntryToReadingList(@Path("id") long listId,
@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingListEntry entry);
@Headers("Cache-Control: no-cache")
@POST("data/lists/{id}/entries/batch")
@NonNull Call<SyncedReadingLists.RemoteIdResponseBatch> addEntriesToReadingList(@Path("id") long listId,
@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingListEntryBatch batch);
@Headers("Cache-Control: no-cache")
@DELETE("data/lists/{id}/entries/{entry_id}")
@NonNull Call<Void> deleteEntryFromReadingList(@Path("id") long listId, @Path("entry_id") long entryId,
@Query("csrf_token") String token);
}

View file

@ -0,0 +1,401 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.captcha.Captcha;
import org.wikipedia.dataclient.mwapi.CreateAccountResponse;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.SiteMatrix;
import org.wikipedia.dataclient.mwapi.page.MwMobileViewPageLead;
import org.wikipedia.dataclient.mwapi.page.MwMobileViewPageRemaining;
import org.wikipedia.dataclient.mwapi.page.MwQueryPageSummary;
import org.wikipedia.edit.Edit;
import org.wikipedia.edit.preview.EditPreview;
import org.wikipedia.login.LoginClient;
import org.wikipedia.search.PrefixSearchResponse;
import org.wikipedia.wikidata.Entities;
import io.reactivex.Observable;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
/**
* Retrofit service layer for all API interactions, including regular MediaWiki and RESTBase.
*/
public interface Service {
String WIKIPEDIA_URL = "https://wikipedia.org/";
String WIKIDATA_URL = "https://www.wikidata.org/";
String COMMONS_URL = "https://commons.wikimedia.org/";
String META_URL = "https://meta.wikimedia.org/";
String MW_API_PREFIX = "w/api.php?format=json&formatversion=2&errorformat=plaintext&";
String MW_PAGE_SECTIONS_URL = MW_API_PREFIX + "action=mobileview&prop="
+ "text|sections&onlyrequestedsections=1&sections=1-"
+ "&sectionprop=toclevel|line|anchor&noheadings=";
int PREFERRED_THUMB_SIZE = 320;
String OFFLINE_SAVE_HEADER = "X-Offline-Save";
String OFFLINE_SAVE_HEADER_SAVE = "save";
String OFFLINE_SAVE_HEADER_DELETE = "delete";
String OFFLINE_SAVE_HEADER_NONE = "none";
// ------- MobileView page content -------
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @return a Retrofit Call which provides the populated MwMobileViewPageLead object in #success
*/
/*
Here's the rationale for this API call:
We request 10 sentences from the lead section, and then re-parse the text using our own
sentence parsing logic to end up with 2 sentences for the link preview. We trust our
parsing logic more than TextExtracts because it's better-tailored to the user's
Locale on the client side. For example, the TextExtracts extension incorrectly treats
abbreviations like "i.e.", "B.C.", "Jr.", etc. as separate sentences, whereas our parser
will leave those alone.
Also, we no longer request "excharacters" from TextExtracts, since it has an issue where
it's liable to return content that lies beyond the lead section, which might include
unparsed wikitext, which we certainly don't want.
*/
@Headers("x-analytics: preview=1")
@GET(MW_API_PREFIX + "action=query&redirects=&converttitles="
+ "&prop=extracts|pageimages|pageprops&exsentences=5&piprop=thumbnail|name"
+ "&pilicense=any&explaintext=&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryPageSummary> getSummary(@Nullable @Header("Referer") String referrerUrl,
@NonNull @Query("titles") String title,
@Nullable @Query("uselang") String useLang);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @param leadImageWidth one of the bucket widths for the lead image
*/
@Headers("x-analytics: pageview=1")
@GET(MW_API_PREFIX + "action=mobileview&prop="
+ "text|sections|languagecount|thumb|image|id|namespace|revision"
+ "|description|lastmodified|normalizedtitle|displaytitle|protection"
+ "|editable|pageprops&pageprops=wikibase_item"
+ "&sections=0&sectionprop=toclevel|line|anchor&noheadings=")
@NonNull Observable<Response<MwMobileViewPageLead>> getLeadSection(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@Nullable @Header("Referer") String referrerUrl,
@NonNull @Query("page") String title,
@Query("thumbwidth") int leadImageWidth,
@Nullable @Query("uselang") String useLang);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@GET(MW_PAGE_SECTIONS_URL)
@NonNull Observable<Response<MwMobileViewPageRemaining>> getRemainingSections(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Query("page") String title,
@Nullable @Query("uselang") String useLang);
/**
* TODO: remove this if we find a way to get the request url before the observable object being executed
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@GET(MW_PAGE_SECTIONS_URL)
@NonNull Call<MwMobileViewPageRemaining> getRemainingSectionsUrl(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Query("page") String title,
@Nullable @Query("uselang") String useLang);
// ------- Search -------
@GET(MW_API_PREFIX + "action=query&prop=pageimages&piprop=thumbnail"
+ "&converttitles=&pilicense=any&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getPageImages(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&redirects="
+ "&converttitles=&prop=description|pageimages&piprop=thumbnail"
+ "&pilicense=any&generator=prefixsearch&gpsnamespace=0&list=search&srnamespace=0"
+ "&srwhat=text&srinfo=suggestion&srprop=&sroffset=0&srlimit=1&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<PrefixSearchResponse> prefixSearch(@Query("gpssearch") String title,
@Query("gpslimit") int maxResults,
@Query("srsearch") String repeat);
@GET(MW_API_PREFIX + "action=query&converttitles="
+ "&prop=description|pageimages|pageprops&ppprop=mainpage|disambiguation"
+ "&generator=search&gsrnamespace=0&gsrwhat=text"
+ "&gsrinfo=&gsrprop=redirecttitle&piprop=thumbnail&pilicense=any&pithumbsize="
+ PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> fullTextSearch(@Query("gsrsearch") String searchTerm,
@Query("gsrlimit") int gsrLimit,
@Query("continue") String cont,
@Query("gsroffset") String gsrOffset);
@GET(MW_API_PREFIX + "action=query&prop=coordinates|description|pageimages"
+ "&colimit=50&piprop=thumbnail&pilicense=any"
+ "&generator=geosearch&ggslimit=50&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> nearbySearch(@NonNull @Query("ggscoord") String coord,
@Query("ggsradius") double radius);
// ------- Miscellaneous -------
@GET(MW_API_PREFIX + "action=fancycaptchareload")
@NonNull Observable<Captcha> getNewCaptcha();
@GET(MW_API_PREFIX + "action=query&prop=langlinks&lllimit=500&redirects=&converttitles=")
@NonNull Observable<MwQueryResponse> getLangLinks(@NonNull @Query("titles") String title);
@GET(MW_API_PREFIX + "action=query&prop=description|pageprops&redirects")
@NonNull Observable<MwQueryResponse> getPagePropsAndDescription(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&prop=description")
@NonNull Observable<MwQueryResponse> getDescription(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&prop=imageinfo&iiprop=timestamp|user|url|extmetadata&iiurlwidth=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getImageExtMetadata(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=sitematrix&smtype=language&smlangprop=code|name|localname")
@NonNull Observable<SiteMatrix> getSiteMatrix();
@GET(MW_API_PREFIX + "action=query&meta=siteinfo")
@NonNull Observable<MwQueryResponse> getSiteInfo();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&generator=random&redirects=1&grnnamespace=0&grnlimit=50&prop=pageprops|description")
@NonNull Observable<MwQueryResponse> getRandomWithPageProps();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&generator=random&redirects=1&grnnamespace=6&grnlimit=50"
+ "&prop=description|imageinfo&iiprop=timestamp|user|url|mime&iiurlwidth=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getRandomWithImageInfo();
@GET(MW_API_PREFIX + "action=query&prop=categories&clprop=hidden&cllimit=500")
@NonNull Observable<MwQueryResponse> getCategories(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&list=categorymembers&cmlimit=500")
@NonNull Observable<MwQueryResponse> getCategoryMembers(@NonNull @Query("cmtitle") String title,
@Nullable @Query("cmcontinue") String continueStr);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=thank")
@NonNull Observable<MwPostResponse> thank(@Nullable @Field("rev") String rev,
@Nullable @Field("log") String log,
@NonNull @Field("token") String token,
@Nullable @Field("source") String source);
// ------- CSRF, Login, and Create Account -------
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull Call<MwQueryResponse> getCsrfTokenCall();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull Observable<MwQueryResponse> getCsrfToken();
@SuppressWarnings("checkstyle:parameternumber")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=createaccount&createmessageformat=html")
@NonNull Observable<CreateAccountResponse> postCreateAccount(@NonNull @Field("username") String user,
@NonNull @Field("password") String pass,
@NonNull @Field("retype") String retype,
@NonNull @Field("createtoken") String token,
@NonNull @Field("createreturnurl") String returnurl,
@Nullable @Field("email") String email,
@Nullable @Field("captchaId") String captchaId,
@Nullable @Field("captchaWord") String captchaWord);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=login")
@NonNull Call<MwQueryResponse> getLoginToken();
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
@NonNull Call<LoginClient.LoginResponse> postLogIn(@Field("username") String user, @Field("password") String pass,
@Field("logintoken") String token, @Field("loginreturnurl") String url);
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
@NonNull Call<LoginClient.LoginResponse> postLogIn(@Field("username") String user, @Field("password") String pass,
@Field("retype") String retypedPass, @Field("OATHToken") String twoFactorCode,
@Field("logintoken") String token,
@Field("logincontinue") boolean loginContinue);
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=logout")
@NonNull Observable<MwPostResponse> postLogout(@NonNull @Field("token") String token);
@GET(MW_API_PREFIX + "action=query&meta=authmanagerinfo|tokens&amirequestsfor=create&type=createaccount")
@NonNull Observable<MwQueryResponse> getAuthManagerInfo();
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
@NonNull Observable<MwQueryResponse> getUserInfo(@Query("ususers") @NonNull String userName);
// ------- Notifications -------
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notformat=model&notlimit=max")
@NonNull Observable<MwQueryResponse> getAllNotifications(@Query("notwikis") @Nullable String wikiList,
@Query("notfilter") @Nullable String filter,
@Query("notcontinue") @Nullable String continueStr);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=echomarkread")
@NonNull Observable<MwQueryResponse> markRead(@Field("token") @NonNull String token, @Field("list") @Nullable String readList, @Field("unreadlist") @Nullable String unreadList);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notprop=list&notfilter=!read&notlimit=1")
@NonNull Observable<MwQueryResponse> getLastUnreadNotification();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=unreadnotificationpages&unplimit=max&unpwikis=*")
@NonNull Observable<MwQueryResponse> getUnreadNotificationWikis();
// ------- User Options -------
@GET(MW_API_PREFIX + "action=query&meta=userinfo&uiprop=options")
@NonNull Observable<MwQueryResponse> getUserOptions();
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=options")
@NonNull Observable<MwPostResponse> postUserOption(@Field("token") @NonNull String token,
@Query("optionname") @NonNull String key,
@Query("optionvalue") @Nullable String value);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=options")
@NonNull Observable<MwPostResponse> deleteUserOption(@Field("token") @NonNull String token,
@Query("change") @NonNull String key);
// ------- Editing -------
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
@NonNull Observable<MwQueryResponse> getWikiTextForSection(@NonNull @Query("titles") String title, @Query("rvsection") int section);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=parse&prop=text&sectionpreview=&pst=&mobileformat=")
@NonNull Observable<EditPreview> postEditPreview(@NonNull @Field("title") String title,
@NonNull @Field("text") String text);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@SuppressWarnings("checkstyle:parameternumber")
@NonNull Call<Edit> postEditSubmit(@NonNull @Field("title") String title,
@Nullable @Field("section") Integer section,
@NonNull @Field("summary") String summary,
@Nullable @Field("assert") String user,
@NonNull @Field("text") String text,
@Nullable @Field("basetimestamp") String baseTimeStamp,
@NonNull @Field("token") String token,
@Nullable @Field("captchaid") String captchaId,
@Nullable @Field("captchaword") String captchaWord);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@NonNull Observable<Edit> postAppendEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("appendtext") String text,
@NonNull @Field("token") String token);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@NonNull Observable<Edit> postPrependEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("prependtext") String text,
@NonNull @Field("token") String token);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=tag")
@FormUrlEncoded
Observable<MwPostResponse> addEditTag(@NonNull @Field("revid") String revId,
@NonNull @Field("add") String tagName,
@NonNull @Field("reason") String reason,
@NonNull @Field("token") String token);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=wikimediaeditortaskscounts")
@NonNull Observable<MwQueryResponse> getEditorTaskCounts();
@GET(MW_API_PREFIX + "action=query&generator=wikimediaeditortaskssuggestions&prop=pageprops&gwetstask=missingdescriptions&gwetslimit=3")
@NonNull Observable<MwQueryResponse> getEditorTaskMissingDescriptions(@NonNull @Query("gwetstarget") String targetLanguage);
@GET(MW_API_PREFIX + "action=query&generator=wikimediaeditortaskssuggestions&prop=pageprops&gwetstask=descriptiontranslations&gwetslimit=3")
@NonNull Observable<MwQueryResponse> getEditorTaskTranslatableDescriptions(@NonNull @Query("gwetssource") String sourceLanguage,
@NonNull @Query("gwetstarget") String targetLanguage);
// ------- Wikidata -------
@GET(MW_API_PREFIX + "action=wbgetentities")
@NonNull Observable<Entities> getEntitiesByTitle(@Query("titles") @NonNull String titles,
@Query("sites") @NonNull String sites);
@GET(MW_API_PREFIX + "action=wbgetentities&props=labels&languagefallback=1")
@NonNull Call<Entities> getWikidataLabels(@Query("ids") @NonNull String idList,
@Query("languages") @NonNull String langList);
@GET(MW_API_PREFIX + "action=wbgetentities&props=descriptions|labels|sitelinks")
@NonNull Observable<Entities> getWikidataLabelsAndDescriptions(@Query("ids") @NonNull String idList);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbsetdescription&errorlang=uselang")
@FormUrlEncoded
@SuppressWarnings("checkstyle:parameternumber")
Observable<MwPostResponse> postDescriptionEdit(@NonNull @Field("language") String language,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("site") String site,
@NonNull @Field("title") String title,
@NonNull @Field("value") String newDescription,
@Nullable @Field("summary") String summary,
@NonNull @Field("token") String token,
@Nullable @Field("assert") String user);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbsetlabel&errorlang=uselang")
@FormUrlEncoded
@SuppressWarnings("checkstyle:parameternumber")
Observable<MwPostResponse> postLabelEdit(@NonNull @Field("language") String language,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("site") String site,
@NonNull @Field("title") String title,
@NonNull @Field("value") String newDescription,
@Nullable @Field("summary") String summary,
@NonNull @Field("token") String token,
@Nullable @Field("assert") String user);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbcreateclaim&errorlang=uselang")
@FormUrlEncoded
Observable<MwPostResponse> postCreateClaim(@NonNull @Field("entity") String entity,
@NonNull @Field("snaktype") String snakType,
@NonNull @Field("property") String property,
@NonNull @Field("value") String value,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("token") String token);
}

View file

@ -0,0 +1,12 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
/**
* The API reported an error in the payload.
*/
public interface ServiceError {
@NonNull String getTitle();
@NonNull String getDetails();
}

View file

@ -0,0 +1,68 @@
package org.wikipedia.dataclient;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import org.wikipedia.AppAdapter;
import org.wikipedia.json.GsonUtil;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public final class ServiceFactory {
private static final int SERVICE_CACHE_SIZE = 8;
private static LruCache<Long, Service> SERVICE_CACHE = new LruCache<>(SERVICE_CACHE_SIZE);
private static LruCache<Long, RestService> REST_SERVICE_CACHE = new LruCache<>(SERVICE_CACHE_SIZE);
public static Service get(@NonNull WikiSite wiki) {
long hashCode = wiki.hashCode();
if (SERVICE_CACHE.get(hashCode) != null) {
return SERVICE_CACHE.get(hashCode);
}
Retrofit r = createRetrofit(wiki, wiki.url() + "/");
Service s = r.create(Service.class);
SERVICE_CACHE.put(hashCode, s);
return s;
}
public static <T> T get(@NonNull WikiSite wiki, Class<T> service) {
return get(wiki, wiki.url() + "/", service);
}
public static <T> T get(@NonNull WikiSite wiki, @Nullable String baseUrl, Class<T> service) {
Retrofit r = createRetrofit(wiki, TextUtils.isEmpty(baseUrl) ? wiki.url() + "/" : baseUrl);
return r.create(service);
}
public static RestService getRest(@NonNull WikiSite wiki) {
long hashCode = wiki.hashCode();
if (REST_SERVICE_CACHE.get(hashCode) != null) {
return REST_SERVICE_CACHE.get(hashCode);
}
Retrofit r = createRetrofit(wiki, TextUtils.isEmpty(AppAdapter.get().getRestbaseUriFormat())
? wiki.url() + "/" + RestService.REST_API_PREFIX
: String.format(AppAdapter.get().getRestbaseUriFormat(), "https", wiki.authority()));
RestService s = r.create(RestService.class);
REST_SERVICE_CACHE.put(hashCode, s);
return s;
}
private static Retrofit createRetrofit(@NonNull WikiSite wiki, @NonNull String baseUrl) {
return new Retrofit.Builder()
.client(AppAdapter.get().getOkHttpClient(wiki))
.baseUrl(baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
.build();
}
private ServiceFactory() { }
}

View file

@ -0,0 +1,166 @@
package org.wikipedia.dataclient;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.AppAdapter;
import org.wikipedia.util.log.L;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
public final class SharedPreferenceCookieManager implements CookieJar {
private static final String CENTRALAUTH_PREFIX = "centralauth_";
private static SharedPreferenceCookieManager INSTANCE;
// Map: domain -> list of cookies
private final Map<String, List<Cookie>> cookieJar;
@NonNull
public static SharedPreferenceCookieManager getInstance() {
if (INSTANCE == null) {
try {
INSTANCE = AppAdapter.get().getCookies();
} catch (Exception e) {
L.logRemoteErrorIfProd(e);
}
}
if (INSTANCE == null) {
INSTANCE = new SharedPreferenceCookieManager();
}
return INSTANCE;
}
public SharedPreferenceCookieManager(Map<String, List<Cookie>> cookieJar) {
this.cookieJar = cookieJar;
}
private SharedPreferenceCookieManager() {
cookieJar = new HashMap<>();
}
public Map<String, List<Cookie>> getCookieJar() {
return cookieJar;
}
private void persistCookies() {
AppAdapter.get().setCookies(this);
}
public synchronized void clearAllCookies() {
cookieJar.clear();
persistCookies();
}
@Nullable public synchronized String getCookieByName(@NonNull String name) {
for (String domainSpec: cookieJar.keySet()) {
for (Cookie cookie : cookieJar.get(domainSpec)) {
if (cookie.name().equals(name)) {
return cookie.value();
}
}
}
return null;
}
@Override
public synchronized void saveFromResponse(@NonNull HttpUrl url, @NonNull List<Cookie> cookies) {
if (cookies.isEmpty()) {
return;
}
boolean cookieJarModified = false;
for (Cookie cookie : cookies) {
// Default to the URI's domain if cookie's domain is not explicitly set
String domainSpec = TextUtils.isEmpty(cookie.domain()) ? url.uri().getAuthority() : cookie.domain();
if (!cookieJar.containsKey(domainSpec)) {
cookieJar.put(domainSpec, new ArrayList<>());
}
List<Cookie> cookieList = cookieJar.get(domainSpec);
if (cookie.expiresAt() < System.currentTimeMillis() || "deleted".equals(cookie.value())) {
Iterator<Cookie> i = cookieList.iterator();
while (i.hasNext()) {
if (i.next().name().equals(cookie.name())) {
i.remove();
cookieJarModified = true;
}
}
} else {
Iterator<Cookie> i = cookieList.iterator();
boolean exists = false;
while (i.hasNext()) {
Cookie c = i.next();
if (c.equals(cookie)) {
// an identical cookie already exists, so we don't need to update it.
exists = true;
break;
} else if (c.name().equals(cookie.name())) {
// it's a cookie with the same name, but different contents, so remove the
// current cookie, so that the new one will be added.
i.remove();
}
}
if (!exists) {
cookieList.add(cookie);
cookieJarModified = true;
}
}
}
if (cookieJarModified) {
persistCookies();
}
}
@Override
public synchronized List<Cookie> loadForRequest(@NonNull HttpUrl url) {
List<Cookie> cookieList = new ArrayList<>();
String domain = url.uri().getAuthority();
Log.d("CookieManager", "Domain:" + domain);
for (String domainSpec : cookieJar.keySet()) {
List<Cookie> cookiesForDomainSpec = cookieJar.get(domainSpec);
if (domain.endsWith(domainSpec)) {
buildCookieList(cookieList, cookiesForDomainSpec, null);
} else if (domainSpec.endsWith("commons.wikimedia.org")) {
Log.d("CookieManager", "Adding centralauth cookies");
// For sites outside the wikipedia.org domain, transfer the centralauth cookies
// from commons.wikimedia.org unconditionally.
buildCookieList(cookieList, cookiesForDomainSpec, CENTRALAUTH_PREFIX);
}
}
return cookieList;
}
private void buildCookieList(@NonNull List<Cookie> outList, @NonNull List<Cookie> inList, @Nullable String prefix) {
Iterator<Cookie> i = inList.iterator();
boolean cookieJarModified = false;
while (i.hasNext()) {
Cookie cookie = i.next();
if (prefix != null && !cookie.name().startsWith(prefix)) {
continue;
}
// But wait, is the cookie expired?
if (cookie.expiresAt() < System.currentTimeMillis()) {
i.remove();
cookieJarModified = true;
} else {
outList.add(cookie);
}
}
if (cookieJarModified) {
persistCookies();
}
}
}

View file

@ -0,0 +1,320 @@
package org.wikipedia.dataclient;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.page.PageTitle;
import org.wikipedia.util.UriUtil;
/**
* The base URL and Wikipedia language code for a MediaWiki site. Examples:
*
* <ul>
* <lh>Name: scheme / authority / language code</lh>
* <li>English Wikipedia: HTTPS / en.wikipedia.org / en</li>
* <li>Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant</li>
* <li>Meta-Wiki: HTTPS / meta.wikimedia.org / (none)</li>
* <li>Test Wikipedia: HTTPS / test.wikipedia.org / test</li>
* <li>Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro</li>
* <li>Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple</li>
* <li>Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple</li>
* <li>Development: HTTP / 192.168.1.11:8080 / (none)</li>
* </ul>
*
* <strong>As shown above, the language code or mapping is part of the authority:</strong>
* <ul>
* <lh>Validity: authority / language code</lh>
* <li>Correct: "test.wikipedia.org" / "test"</li>
* <li>Correct: "wikipedia.org", ""</li>
* <li>Correct: "no.wikipedia.org", "nb"</li>
* <li>Incorrect: "wikipedia.org", "test"</li>
* </ul>
*/
public class WikiSite implements Parcelable {
public static final String DEFAULT_SCHEME = "https";
private static String DEFAULT_BASE_URL = Service.WIKIPEDIA_URL;
public static final Parcelable.Creator<WikiSite> CREATOR = new Parcelable.Creator<WikiSite>() {
@Override
public WikiSite createFromParcel(Parcel in) {
return new WikiSite(in);
}
@Override
public WikiSite[] newArray(int size) {
return new WikiSite[size];
}
};
// todo: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added
@SerializedName("domain") @NonNull private final Uri uri;
@NonNull private String languageCode;
public static boolean supportedAuthority(@NonNull String authority) {
return authority.endsWith(Uri.parse(DEFAULT_BASE_URL).getAuthority());
}
public static void setDefaultBaseUrl(@NonNull String url) {
DEFAULT_BASE_URL = TextUtils.isEmpty(url) ? Service.WIKIPEDIA_URL : url;
}
public static WikiSite forLanguageCode(@NonNull String languageCode) {
Uri uri = ensureScheme(Uri.parse(DEFAULT_BASE_URL));
return new WikiSite((languageCode.isEmpty()
? "" : (languageCodeToSubdomain(languageCode) + ".")) + uri.getAuthority(),
languageCode);
}
public WikiSite(@NonNull Uri uri) {
Uri tempUri = ensureScheme(uri);
String authority = tempUri.getAuthority();
if (("wikipedia.org".equals(authority) || "www.wikipedia.org".equals(authority))
&& tempUri.getPath() != null && tempUri.getPath().startsWith("/wiki")) {
// Special case for Wikipedia only: assume English subdomain when none given.
authority = "en.wikipedia.org";
}
String langVariant = UriUtil.getLanguageVariantFromUri(tempUri);
if (!TextUtils.isEmpty(langVariant)) {
languageCode = langVariant;
} else {
languageCode = authorityToLanguageCode(authority);
}
this.uri = new Uri.Builder()
.scheme(tempUri.getScheme())
.encodedAuthority(authority)
.build();
}
public WikiSite(@NonNull String url) {
this(url.startsWith("http") ? Uri.parse(url) : url.startsWith("//")
? Uri.parse(DEFAULT_SCHEME + ":" + url) : Uri.parse(DEFAULT_SCHEME + "://" + url));
}
public WikiSite(@NonNull String authority, @NonNull String languageCode) {
this(authority);
this.languageCode = languageCode;
}
@NonNull
public String scheme() {
return TextUtils.isEmpty(uri.getScheme()) ? DEFAULT_SCHEME : uri.getScheme();
}
/**
* @return The complete wiki authority including language subdomain but not including scheme,
* authentication, port, nor trailing slash.
*
* @see <a href='https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax'>URL syntax</a>
*/
@NonNull
public String authority() {
return uri.getAuthority();
}
/**
* Like {@link #authority()} but with a "m." between the language subdomain and the rest of the host.
* Examples:
*
* <ul>
* <li>English Wikipedia: en.m.wikipedia.org</li>
* <li>Chinese Wikipedia: zh.m.wikipedia.org</li>
* <li>Meta-Wiki: meta.m.wikimedia.org</li>
* <li>Test Wikipedia: test.m.wikipedia.org</li>
* <li>Võro Wikipedia: fiu-vro.m.wikipedia.org</li>
* <li>Simple English Wikipedia: simple.m.wikipedia.org</li>
* <li>Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org</li>
* <li>Development: m.192.168.1.11</li>
* </ul>
*/
@NonNull
public String mobileAuthority() {
return authorityToMobile(authority());
}
/**
* @return The canonical "desktop" form of the authority. For example, if the authority
* is in a "mobile" form, e.g. en.m.wikipedia.org, this will become en.wikipedia.org.
*/
@NonNull
public String desktopAuthority() {
return authority().replace(".m.", ".");
}
@NonNull
public String subdomain() {
return languageCodeToSubdomain(languageCode);
}
/**
* @return A path without an authority for the segment including a leading "/".
*/
@NonNull
public String path(@NonNull String segment) {
return "/w/" + segment;
}
@NonNull public Uri uri() {
return uri;
}
/**
* @return The canonical URL. e.g., https://en.wikipedia.org.
*/
@NonNull public String url() {
return uri.toString();
}
/**
* @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo.
*/
@NonNull public String url(@NonNull String segment) {
return url() + path(segment);
}
/**
* @return The wiki language code which may differ from the language subdomain. Empty if
* language code is unknown. Ex: "en", "zh-hans", ""
*
* @see AppLanguageLookUpTable
*/
@NonNull
public String languageCode() {
return languageCode;
}
// TODO: this method doesn't have much to do with WikiSite. Move to PageTitle?
/**
* Create a PageTitle object from an internal link string.
*
* @param internalLink Internal link target text (eg. /wiki/Target).
* Should be URL decoded before passing in
* @return A {@link PageTitle} object representing the internalLink passed in.
*/
public PageTitle titleForInternalLink(String internalLink) {
// Strip the /wiki/ from the href
return new PageTitle(UriUtil.removeInternalLinkPrefix(internalLink), this);
}
// TODO: this method doesn't have much to do with WikiSite. Move to PageTitle?
/**
* Create a PageTitle object from a Uri, taking into account any fragment (section title) in the link.
* @param uri Uri object to be turned into a PageTitle.
* @return {@link PageTitle} object that corresponds to the given Uri.
*/
public PageTitle titleForUri(Uri uri) {
String path = uri.getPath();
if (!TextUtils.isEmpty(uri.getFragment())) {
path += "#" + uri.getFragment();
}
return titleForInternalLink(path);
}
@NonNull public String dbName() {
return subdomain().replaceAll("-", "_") + "wiki";
}
// Auto-generated
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WikiSite wiki = (WikiSite) o;
if (!uri.equals(wiki.uri)) {
return false;
}
return languageCode.equals(wiki.languageCode);
}
// Auto-generated
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + languageCode.hashCode();
return result;
}
// Auto-generated
@Override
public String toString() {
return "WikiSite{"
+ "uri=" + uri
+ ", languageCode='" + languageCode + '\''
+ '}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(uri, 0);
dest.writeString(languageCode);
}
protected WikiSite(@NonNull Parcel in) {
this.uri = in.readParcelable(Uri.class.getClassLoader());
this.languageCode = in.readString();
}
@NonNull
private static String languageCodeToSubdomain(@NonNull String languageCode) {
switch (languageCode) {
case AppLanguageLookUpTable.SIMPLIFIED_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.TRADITIONAL_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_CN_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_HK_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_MO_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_SG_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_TW_LANGUAGE_CODE:
return AppLanguageLookUpTable.CHINESE_LANGUAGE_CODE;
case AppLanguageLookUpTable.NORWEGIAN_BOKMAL_LANGUAGE_CODE:
return AppLanguageLookUpTable.NORWEGIAN_LEGACY_LANGUAGE_CODE; // T114042
default:
return languageCode;
}
}
@NonNull private static String authorityToLanguageCode(@NonNull String authority) {
String[] parts = authority.split("\\.");
final int minLengthForSubdomain = 3;
if (parts.length < minLengthForSubdomain
|| parts.length == minLengthForSubdomain && parts[0].equals("m")) {
// ""
// wikipedia.org
// m.wikipedia.org
return "";
}
return parts[0];
}
@NonNull private static Uri ensureScheme(@NonNull Uri uri) {
if (TextUtils.isEmpty(uri.getScheme())) {
return uri.buildUpon().scheme(DEFAULT_SCHEME).build();
}
return uri;
}
/** @param authority Host and optional port. */
@NonNull private String authorityToMobile(@NonNull String authority) {
if (authority.startsWith("m.") || authority.contains(".m.")) {
return authority;
}
return authority.replaceFirst("^" + subdomain() + "\\.?", "$0m.");
}
}

View file

@ -0,0 +1,42 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CreateAccountResponse extends MwResponse {
@SuppressWarnings("unused") @Nullable private Result createaccount;
@Nullable public String status() {
return createaccount.status();
}
@Nullable public String user() {
return createaccount.user();
}
@Nullable public String message() {
return createaccount.message();
}
public boolean hasResult() {
return createaccount != null;
}
public static class Result {
@SuppressWarnings("unused,NullableProblems") @NonNull private String status;
@SuppressWarnings("unused") @Nullable private String message;
@SuppressWarnings("unused") @Nullable private String username;
@NonNull public String status() {
return status;
}
@Nullable public String user() {
return username;
}
@Nullable public String message() {
return message;
}
}
}

View file

@ -0,0 +1,113 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.GsonUtil;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class EditorTaskCounts {
@Nullable private JsonElement counts;
@Nullable @SerializedName("targets_passed") private JsonElement targetsPassed;
@Nullable private JsonElement targets;
@NonNull
public Map<String, Integer> getDescriptionEditsPerLanguage() {
Map<String, Integer> editsPerLanguage = null;
if (counts != null && !(counts instanceof JsonArray)) {
editsPerLanguage = GsonUtil.getDefaultGson().fromJson(counts, Counts.class).appDescriptionEdits;
}
return editsPerLanguage == null ? Collections.emptyMap() : editsPerLanguage;
}
@NonNull
public List<Integer> getDescriptionEditTargetsPassed() {
List<Integer> passedList = null;
if (targetsPassed != null && !(targetsPassed instanceof JsonArray)) {
passedList = GsonUtil.getDefaultGson().fromJson(targetsPassed, Targets.class).appDescriptionEdits;
}
return passedList == null ? Collections.emptyList() : passedList;
}
public int getDescriptionEditTargetsPassedCount() {
List<Integer> targetList = getDescriptionEditTargets();
List<Integer> passedList = getDescriptionEditTargetsPassed();
int count = 0;
if (!targetList.isEmpty() && !passedList.isEmpty()) {
for (int target : targetList) {
if (passedList.contains(target)) {
count++;
}
}
}
return count;
}
@NonNull
public List<Integer> getDescriptionEditTargets() {
List<Integer> targetList = null;
if (targets != null && !(targets instanceof JsonArray)) {
targetList = GsonUtil.getDefaultGson().fromJson(targets, Targets.class).appDescriptionEdits;
}
return targetList == null ? Collections.emptyList() : targetList;
}
@NonNull
public Map<String, Integer> getCaptionEditsPerLanguage() {
Map<String, Integer> editsPerLanguage = null;
if (counts != null && !(counts instanceof JsonArray)) {
editsPerLanguage = GsonUtil.getDefaultGson().fromJson(counts, Counts.class).appCaptionEdits;
}
return editsPerLanguage == null ? Collections.emptyMap() : editsPerLanguage;
}
@NonNull
public List<Integer> getCaptionEditTargetsPassed() {
List<Integer> passedList = null;
if (targetsPassed != null && !(targetsPassed instanceof JsonArray)) {
passedList = GsonUtil.getDefaultGson().fromJson(targetsPassed, Targets.class).appCaptionEdits;
}
return passedList == null ? Collections.emptyList() : passedList;
}
public int getCaptionEditTargetsPassedCount() {
List<Integer> targetList = getCaptionEditTargets();
List<Integer> passedList = getCaptionEditTargetsPassed();
int count = 0;
if (!targetList.isEmpty() && !passedList.isEmpty()) {
for (int target : targetList) {
if (passedList.contains(target)) {
count++;
}
}
}
return count;
}
@NonNull
public List<Integer> getCaptionEditTargets() {
List<Integer> targetList = null;
if (targets != null && !(targets instanceof JsonArray)) {
targetList = GsonUtil.getDefaultGson().fromJson(targets, Targets.class).appCaptionEdits;
}
return targetList == null ? Collections.emptyList() : targetList;
}
public class Counts {
@Nullable @SerializedName("app_description_edits") private Map<String, Integer> appDescriptionEdits;
@Nullable @SerializedName("app_caption_edits") private Map<String, Integer> appCaptionEdits;
}
public class Targets {
@Nullable @SerializedName("app_description_edits") private List<Integer> appDescriptionEdits;
@Nullable @SerializedName("app_caption_edits") private List<Integer> appCaptionEdits;
}
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class GeoSearchItem {
@Nullable private String title;
@SerializedName("lat") private double latitude;
@SerializedName("lon") private double longitude;
@SerializedName("dist") private double distance;
@NonNull public String getTitle() {
return StringUtils.defaultString(title);
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public double getDistance() {
return distance;
}
}

View file

@ -0,0 +1,17 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
public class ImageDetails {
@SuppressWarnings("unused") private String name;
@SuppressWarnings("unused") private String title;
@NonNull public String getName() {
return name;
}
@NonNull public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,44 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@SuppressWarnings("unused")
public class ListUserResponse {
@SerializedName("name") @Nullable private String name;
private long userid;
@Nullable private List<String> groups;
@Nullable private String cancreate;
@Nullable private List<UserResponseCreateError> cancreateerror;
@Nullable public String name() {
return name;
}
public boolean canCreate() {
return cancreate != null;
}
@NonNull public Set<String> getGroups() {
return groups != null ? new ArraySet<>(groups) : Collections.emptySet();
}
public static class UserResponseCreateError {
@Nullable private String message;
@Nullable private String code;
@Nullable private String type;
@NonNull public String message() {
return StringUtils.defaultString(message);
}
}
}

View file

@ -0,0 +1,47 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.model.BaseModel;
import java.util.List;
import java.util.Map;
class MwAuthManagerInfo extends BaseModel {
@SuppressWarnings("unused,NullableProblems") @NonNull private List<Request> requests;
@NonNull List<Request> requests() {
return requests;
}
static class Request {
@SuppressWarnings("unused,NullableProblems") @NonNull private String id;
@SuppressWarnings("unused,NullableProblems") @NonNull private Map<String, String> metadata;
@SuppressWarnings("unused,NullableProblems") @NonNull private String required;
@SuppressWarnings("unused,NullableProblems") @NonNull private String provider;
@SuppressWarnings("unused,NullableProblems") @NonNull private String account;
@SuppressWarnings("unused,NullableProblems") @NonNull private Map<String, Field> fields;
@NonNull String id() {
return id;
}
@NonNull Map<String, Field> fields() {
return fields;
}
}
static class Field {
@SuppressWarnings("unused") @Nullable private String type;
@SuppressWarnings("unused") @Nullable private String value;
@SuppressWarnings("unused") @Nullable private String label;
@SuppressWarnings("unused") @Nullable private String help;
@SuppressWarnings("unused") private boolean optional;
@SuppressWarnings("unused") private boolean sensitive;
@Nullable String value() {
return value;
}
}
}

View file

@ -0,0 +1,24 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class MwException extends RuntimeException {
@SuppressWarnings("unused") @NonNull private final MwServiceError error;
public MwException(@NonNull MwServiceError error) {
this.error = error;
}
@NonNull public MwServiceError getError() {
return error;
}
@Nullable public String getTitle() {
return error.getTitle();
}
@Override @Nullable public String getMessage() {
return error.getDetails();
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.Nullable;
public class MwPostResponse extends MwResponse {
@Nullable @SuppressWarnings("unused") private String options;
@SuppressWarnings("unused") private int success;
public boolean success(@Nullable String result) {
return "success".equals(result);
}
@Nullable public String getOptions() {
return options;
}
public int getSuccessVal() {
return success;
}
}

View file

@ -0,0 +1,107 @@
package org.wikipedia.dataclient.mwapi;
import org.wikipedia.model.BaseModel;
import org.wikipedia.util.DateUtil;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
@SuppressWarnings("unused")
public class MwQueryLogEvent extends BaseModel {
private int logid;
private int ns;
private int index;
private String title;
private int pageid;
private Params params;
private String type;
private String action;
private String user;
private int userid;
private String timestamp;
private String comment;
private String parsedcomment;
private List<String> tags;
public int logid() {
return logid;
}
public int ns() {
return ns;
}
public int index() {
return index;
}
public String title() {
return title;
}
public int pageid() {
return pageid;
}
public String type() {
return type;
}
public String action() {
return action;
}
public String user() {
return user;
}
public int userid() {
return userid;
}
public String timestamp() {
return timestamp;
}
public Date date(){
try {
return DateUtil.iso8601DateParse(timestamp);
} catch (ParseException e) {
return null;
}
}
public String comment() {
return comment;
}
public String parsedcomment() {
return parsedcomment;
}
public List<String> tags() {
return tags;
}
public boolean isDeleted() {
return pageid==0;
}
public Params params() {
return params;
}
public static class Params{
private String img_sha1;
private String img_timestamp;
public String img_sha1() {
return img_sha1;
}
public String img_timestamp() {
return img_timestamp;
}
}
}

View file

@ -0,0 +1,237 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.gallery.VideoInfo;
import org.wikipedia.model.BaseModel;
import org.wikipedia.page.Namespace;
import java.util.Collections;
import java.util.List;
/**
* A class representing a standard page object as returned by the MediaWiki API.
*/
public class MwQueryPage extends BaseModel {
@SuppressWarnings("unused") private int pageid;
@SuppressWarnings("unused") private int ns;
@SuppressWarnings("unused") private int index;
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@SuppressWarnings("unused") @Nullable private List<LangLink> langlinks;
@SuppressWarnings("unused") @Nullable private List<Revision> revisions;
@SuppressWarnings("unused") @Nullable private List<Coordinates> coordinates;
@SuppressWarnings("unused") @Nullable private List<Category> categories;
@SuppressWarnings("unused") @Nullable private PageProps pageprops;
@SuppressWarnings("unused") @Nullable private PageTerms terms;
@SuppressWarnings("unused") @Nullable private String extract;
@SuppressWarnings("unused") @Nullable private Thumbnail thumbnail;
@SuppressWarnings("unused") @Nullable private String description;
@SuppressWarnings("unused") @SerializedName("descriptionsource") @Nullable private String descriptionSource;
@SuppressWarnings("unused") @SerializedName("imageinfo") @Nullable private List<ImageInfo> imageInfo;
@SuppressWarnings("unused") @SerializedName("videoinfo") @Nullable private List<VideoInfo> videoInfo;
@Nullable private String redirectFrom;
@Nullable private String convertedFrom;
@Nullable private String convertedTo;
@NonNull public String title() {
return title;
}
public int index() {
return index;
}
@NonNull public Namespace namespace() {
return Namespace.of(ns);
}
@Nullable public List<LangLink> langLinks() {
return langlinks;
}
@Nullable public List<Revision> revisions() {
return revisions;
}
@Nullable public List<Category> categories() {
return categories;
}
@Nullable public List<Coordinates> coordinates() {
// TODO: Handle null values in lists during deserialization, perhaps with a new
// @RequiredElements annotation and corresponding TypeAdapter
if (coordinates != null) {
coordinates.removeAll(Collections.singleton(null));
}
return coordinates;
}
public List<String> labels() {
return terms != null && terms.label != null ? terms.label : Collections.emptyList();
}
public int pageId() {
return pageid;
}
@Nullable public PageProps pageProps() {
return pageprops;
}
@Nullable public String extract() {
return extract;
}
@Nullable public String thumbUrl() {
return thumbnail != null ? thumbnail.source() : null;
}
@Nullable public String description() {
return description;
}
@Nullable
public String descriptionSource() {
return descriptionSource;
}
@Nullable public ImageInfo imageInfo() {
return imageInfo != null ? imageInfo.get(0) : null;
}
@Nullable public VideoInfo videoInfo() {
return videoInfo != null ? videoInfo.get(0) : null;
}
@Nullable public String redirectFrom() {
return redirectFrom;
}
public void redirectFrom(@Nullable String from) {
redirectFrom = from;
}
@Nullable public String convertedFrom() {
return convertedFrom;
}
public void convertedFrom(@Nullable String from) {
convertedFrom = from;
}
@Nullable public String convertedTo() {
return convertedTo;
}
public void convertedTo(@Nullable String to) {
convertedTo = to;
}
public void appendTitleFragment(@Nullable String fragment) {
title += "#" + fragment;
}
public static class Revision {
@SerializedName("revid") private long revisionId;
private String user;
@SuppressWarnings("unused,NullableProblems") @SerializedName("contentformat") @NonNull private String contentFormat;
@SuppressWarnings("unused,NullableProblems") @SerializedName("contentmodel") @NonNull private String contentModel;
@SuppressWarnings("unused,NullableProblems") @SerializedName("timestamp") @NonNull private String timeStamp;
@SuppressWarnings("unused,NullableProblems") @NonNull private String content;
@NonNull public String content() {
return content;
}
@NonNull public String timeStamp() {
return StringUtils.defaultString(timeStamp);
}
public long getRevisionId() {
return revisionId;
}
@NonNull
public String getUser() {
return StringUtils.defaultString(user);
}
}
public static class LangLink {
@SuppressWarnings("unused,NullableProblems") @NonNull private String lang;
@NonNull public String lang() {
return lang;
}
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@NonNull public String title() {
return title;
}
}
public static class Coordinates {
@SuppressWarnings("unused") @Nullable private Double lat;
@SuppressWarnings("unused") @Nullable private Double lon;
@Nullable public Double lat() {
return lat;
}
@Nullable public Double lon() {
return lon;
}
}
static class Thumbnail {
@SuppressWarnings("unused") private String source;
@SuppressWarnings("unused") private int width;
@SuppressWarnings("unused") private int height;
String source() {
return source;
}
}
public static class PageProps {
@SuppressWarnings("unused") @SerializedName("wikibase_item") @Nullable private String wikiBaseItem;
@SuppressWarnings("unused") @Nullable private String displaytitle;
@SuppressWarnings("unused") @Nullable private String disambiguation;
@Nullable public String getDisplayTitle() {
return displaytitle;
}
@NonNull public String getWikiBaseItem() {
return StringUtils.defaultString(wikiBaseItem);
}
public boolean isDisambiguation() {
return disambiguation != null;
}
}
public static class Category {
@SuppressWarnings("unused") private int ns;
@SuppressWarnings("unused,NullableProblems") @Nullable private String title;
@SuppressWarnings("unused") private boolean hidden;
public int ns() {
return ns;
}
@NonNull public String title() {
return StringUtils.defaultString(title);
}
public boolean hidden() {
return hidden;
}
}
public static class PageTerms {
@SuppressWarnings("unused") private List<String> alias;
@SuppressWarnings("unused") private List<String> label;
}
}

View file

@ -0,0 +1,37 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class MwQueryResponse extends MwResponse {
@SuppressWarnings("unused") @SerializedName("batchcomplete") private boolean batchComplete;
@SuppressWarnings("unused") @SerializedName("continue") @Nullable private Map<String, String> continuation;
@Nullable private MwQueryResult query;
public boolean batchComplete() {
return batchComplete;
}
@Nullable public Map<String, String> continuation() {
return continuation;
}
@Nullable public MwQueryResult query() {
return query;
}
public boolean success() {
return query != null;
}
@VisibleForTesting protected void setQuery(@Nullable MwQueryResult query) {
this.query = query;
}
}

View file

@ -0,0 +1,314 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.gallery.VideoInfo;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.model.BaseModel;
import org.wikipedia.notifications.Notification;
import org.wikipedia.page.PageTitle;
import org.wikipedia.settings.SiteInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class MwQueryResult extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@Nullable private List<MwQueryPage> pages;
@Nullable private List<Redirect> redirects;
@Nullable private List<ConvertedTitle> converted;
@SerializedName("userinfo") private UserInfo userInfo;
@Nullable private List<ListUserResponse> users;
@Nullable private Tokens tokens;
@SerializedName("authmanagerinfo") @Nullable private MwAuthManagerInfo amInfo;
@Nullable private MarkReadResponse echomarkread;
@Nullable private MarkReadResponse echomarkseen;
@Nullable private NotificationList notifications;
@Nullable private Map<String, Notification.UnreadNotificationWikiItem> unreadnotificationpages;
@SerializedName("general") @Nullable private SiteInfo generalSiteInfo;
@Nullable private List<RecentChange> recentchanges;
@SerializedName("wikimediaeditortaskscounts") @Nullable private EditorTaskCounts editorTaskCounts;
@SerializedName("allimages") @Nullable private List<ImageDetails> allImages;
@SerializedName("geosearch") @Nullable private List<GeoSearchItem> geoSearch;
@Nullable private List<MwQueryLogEvent> logevents;
@Nullable public List<MwQueryPage> pages() {
return pages;
}
@Nullable public MwQueryPage firstPage() {
if (pages != null && pages.size() > 0) {
return pages.get(0);
}
return null;
}
@NonNull
public List<ImageDetails> allImages() {
return allImages == null ? Collections.emptyList() : allImages;
}
@NonNull
public List<GeoSearchItem> geoSearch() {
return geoSearch == null ? Collections.emptyList() : geoSearch;
}
@Nullable public UserInfo userInfo() {
return userInfo;
}
@Nullable public String csrfToken() {
return tokens != null ? tokens.csrf() : null;
}
@Nullable public String createAccountToken() {
return tokens != null ? tokens.createAccount() : null;
}
@Nullable public String loginToken() {
return tokens != null ? tokens.login() : null;
}
@Nullable public NotificationList notifications() {
return notifications;
}
@Nullable public Map<String, Notification.UnreadNotificationWikiItem> unreadNotificationWikis() {
return unreadnotificationpages;
}
@Nullable public MarkReadResponse getEchoMarkSeen() {
return echomarkseen;
}
@Nullable public String captchaId() {
String captchaId = null;
if (amInfo != null) {
for (MwAuthManagerInfo.Request request : amInfo.requests()) {
if ("CaptchaAuthenticationRequest".equals(request.id())) {
captchaId = request.fields().get("captchaId").value();
}
}
}
return captchaId;
}
@Nullable public List<RecentChange> getRecentChanges() {
return recentchanges;
}
@Nullable public ListUserResponse getUserResponse(@NonNull String userName) {
if (users != null) {
for (ListUserResponse user : users) {
// MediaWiki user names are case sensitive, but the first letter is always capitalized.
if (StringUtils.capitalize(userName).equals(user.name())) {
return user;
}
}
}
return null;
}
@NonNull public Map<String, ImageInfo> images() {
Map<String, ImageInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages) {
if (page.imageInfo() != null) {
result.put(page.title(), page.imageInfo());
}
}
}
return result;
}
@NonNull public Map<String, VideoInfo> videos() {
Map<String, VideoInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages) {
if (page.videoInfo() != null) {
result.put(page.title(), page.videoInfo());
}
}
}
return result;
}
@NonNull public List<PageTitle> langLinks() {
List<PageTitle> result = new ArrayList<>();
if (pages == null || pages.isEmpty() || pages.get(0).langLinks() == null) {
return result;
}
// noinspection ConstantConditions
for (MwQueryPage.LangLink link : pages.get(0).langLinks()) {
PageTitle title = new PageTitle(link.title(), WikiSite.forLanguageCode(link.lang()));
result.add(title);
}
return result;
}
@NonNull public List<NearbyPage> nearbyPages(@NonNull WikiSite wiki) {
List<NearbyPage> result = new ArrayList<>();
if (pages != null) {
for (MwQueryPage page : pages) {
NearbyPage nearbyPage = new NearbyPage(page, wiki);
if (nearbyPage.getLocation() != null) {
result.add(nearbyPage);
}
}
}
return result;
}
@Nullable public SiteInfo siteInfo() {
return generalSiteInfo;
}
@Nullable public EditorTaskCounts editorTaskCounts() {
return editorTaskCounts;
}
@Override
public void postProcess() {
resolveConvertedTitles();
resolveRedirectedTitles();
}
private void resolveRedirectedTitles() {
if (redirects == null || pages == null) {
return;
}
for (MwQueryPage page : pages) {
for (MwQueryResult.Redirect redirect : redirects) {
// TODO: Looks like result pages and redirects can also be matched on the "index"
// property. Confirm in the API docs and consider updating.
if (page.title().equals(redirect.to())) {
page.redirectFrom(redirect.from());
if (redirect.toFragment() != null) {
page.appendTitleFragment(redirect.toFragment());
}
}
}
}
}
private void resolveConvertedTitles() {
if (converted == null || pages == null) {
return;
}
// noinspection ConstantConditions
for (MwQueryResult.ConvertedTitle convertedTitle : converted) {
// noinspection ConstantConditions
for (MwQueryPage page : pages) {
if (page.title().equals(convertedTitle.to())) {
page.convertedFrom(convertedTitle.from());
page.convertedTo(convertedTitle.to());
}
}
}
}
@Nullable
public List<MwQueryLogEvent> logevents() {
return logevents;
}
private static class Redirect {
@SuppressWarnings("unused") private int index;
@SuppressWarnings("unused") @Nullable private String from;
@SuppressWarnings("unused") @Nullable private String to;
@SuppressWarnings("unused") @SerializedName("tofragment") @Nullable private String toFragment;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
@Nullable public String toFragment() {
return toFragment;
}
}
public static class ConvertedTitle {
@SuppressWarnings("unused") @Nullable private String from;
@SuppressWarnings("unused") @Nullable private String to;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
}
private static class Tokens {
@SuppressWarnings("unused,NullableProblems") @SerializedName("csrftoken")
@Nullable private String csrf;
@SuppressWarnings("unused,NullableProblems") @SerializedName("createaccounttoken")
@Nullable private String createAccount;
@SuppressWarnings("unused,NullableProblems") @SerializedName("logintoken")
@Nullable private String login;
@Nullable private String csrf() {
return csrf;
}
@Nullable private String createAccount() {
return createAccount;
}
@Nullable private String login() {
return login;
}
}
public static class MarkReadResponse {
@SuppressWarnings("unused") @Nullable private String result;
@SuppressWarnings("unused,NullableProblems") @Nullable private String timestamp;
@Nullable public String getResult() {
return result;
}
@Nullable public String getTimestamp() {
return timestamp;
}
}
public static class NotificationList {
@SuppressWarnings("unused") private int count;
@SuppressWarnings("unused") private int rawcount;
@SuppressWarnings("unused") @Nullable private Notification.SeenTime seenTime;
@SuppressWarnings("unused") @Nullable private List<Notification> list;
@SuppressWarnings("unused") @SerializedName("continue") @Nullable private String continueStr;
@Nullable public List<Notification> list() {
return list;
}
@Nullable public String getContinue() {
return continueStr;
}
public int getCount() {
return count;
}
@Nullable public Notification.SeenTime getSeenTime() {
return seenTime;
}
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.model.BaseModel;
import java.util.List;
public abstract class MwResponse extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings({"unused"}) @Nullable private List<MwServiceError> errors;
@SuppressWarnings("unused,NullableProblems") @SerializedName("servedby") @NonNull private String servedBy;
@Override
public void postProcess() {
if (errors != null && !errors.isEmpty()) {
throw new MwException(errors.get(0));
}
}
}

View file

@ -0,0 +1,74 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.model.BaseModel;
import java.util.List;
/**
* Gson POJO for a MediaWiki API error.
*/
public class MwServiceError extends BaseModel implements ServiceError {
@SuppressWarnings("unused") @Nullable private String code;
@SuppressWarnings("unused") @Nullable private String text;
@SuppressWarnings("unused") @Nullable private Data data;
@Override @NonNull public String getTitle() {
return StringUtils.defaultString(code);
}
@Override @NonNull public String getDetails() {
return StringUtils.defaultString(text);
}
public boolean badToken() {
return "badtoken".equals(code);
}
public boolean badLoginState() {
return "assertuserfailed".equals(code);
}
public boolean hasMessageName(@NonNull String messageName) {
if (data != null && data.messages() != null) {
for (Message msg : data.messages()) {
if (messageName.equals(msg.name)) {
return true;
}
}
}
return false;
}
@Nullable public String getMessageHtml(@NonNull String messageName) {
if (data != null && data.messages() != null) {
for (Message msg : data.messages()) {
if (messageName.equals(msg.name)) {
return msg.html();
}
}
}
return null;
}
private static final class Data {
@SuppressWarnings("unused") @Nullable private List<Message> messages;
@Nullable private List<Message> messages() {
return messages;
}
}
private static final class Message {
@SuppressWarnings("unused") @Nullable private String name;
@SuppressWarnings("unused") @Nullable private String html;
@NonNull private String html() {
return StringUtils.defaultString(html);
}
}
}

View file

@ -0,0 +1,67 @@
package org.wikipedia.dataclient.mwapi;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
import java.util.List;
public class NearbyPage {
@NonNull private PageTitle title;
@Nullable private Location location;
/** calculated externally */
private int distance;
public NearbyPage(@NonNull MwQueryPage page, @NonNull WikiSite wiki) {
title = new PageTitle(page.title(), wiki);
title.setThumbUrl(page.thumbUrl());
List<MwQueryPage.Coordinates> coordinates = page.coordinates();
if (coordinates == null || coordinates.isEmpty()) {
return;
}
if (coordinates.get(0).lat() != null && coordinates.get(0).lon() != null) {
location = new Location(title.getPrefixedText());
location.setLatitude(coordinates.get(0).lat());
location.setLongitude(coordinates.get(0).lon());
}
}
public NearbyPage(@NonNull PageTitle title, @Nullable Location location) {
this.title = title;
this.location = location;
}
@NonNull public PageTitle getTitle() {
return title;
}
@Nullable public Location getLocation() {
return location;
}
@Override public String toString() {
return "NearbyPage{"
+ "title='" + title + '\''
+ ", thumbUrl='" + title.getThumbUrl() + '\''
+ ", location=" + location + '\''
+ ", distance='" + distance
+ '}';
}
/**
* Returns the distance from the point where the device is.
* Calculated later and can change. Needs to be set first by #setDistance!
*/
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
}

View file

@ -0,0 +1,42 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class RecentChange {
@Nullable private String type;
@Nullable private String title;
private long pageid;
private long revid;
@SerializedName("old_revid") private long oldRevisionId;
@Nullable private String timestamp;
@NonNull public String getType() {
return StringUtils.defaultString(type);
}
@NonNull public String getTitle() {
return StringUtils.defaultString(title);
}
public long getPageId() {
return pageid;
}
public long getRevId() {
return revid;
}
public long getOldRevisionId() {
return oldRevisionId;
}
public String getTimestamp() {
return StringUtils.defaultString(timestamp);
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import com.google.gson.JsonObject;
import org.wikipedia.json.GsonUtil;
import java.util.ArrayList;
import java.util.List;
public class SiteMatrix extends MwResponse {
@SuppressWarnings("unused,NullableProblems") @NonNull private JsonObject sitematrix;
public JsonObject siteMatrix() {
return sitematrix;
}
@SuppressWarnings("unused,NullableProblems")
public class SiteInfo {
@NonNull
private String code;
@NonNull
private String name;
@NonNull
private String localname;
@NonNull
public String code() {
return code;
}
@NonNull
public String name() {
return name;
}
@NonNull
public String localName() {
return localname;
}
}
public static List<SiteInfo> getSites(@NonNull SiteMatrix siteMatrix) {
List<SiteInfo> sites = new ArrayList<>();
// We have to parse the Json manually because the list of SiteInfo objects
// contains a "count" member that prevents it from being able to deserialize
// as a list automatically.
for (String key : siteMatrix.siteMatrix().keySet()) {
if (key.equals("count")) {
continue;
}
SiteInfo info = GsonUtil.getDefaultGson().fromJson(siteMatrix.siteMatrix().get(key), SiteInfo.class);
if (info != null) {
sites.add(info);
}
}
return sites;
}
}

View file

@ -0,0 +1,84 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
public class UserInfo {
@NonNull
private String name;
@NonNull
private int id;
//Block information
private int blockid;
private String blockedby;
private int blockedbyid;
private String blockreason;
private String blocktimestamp;
private String blockexpiry;
// Object type is any JSON type.
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
@Nullable
private Map<String, ?> options;
public int id() {
return id;
}
@NonNull
public Map<String, String> userjsOptions() {
Map<String, String> map = new HashMap<>();
if (options != null) {
for (Map.Entry<String, ?> entry : options.entrySet()) {
if (entry.getKey().startsWith("userjs-")) {
// T161866 entry.valueOf() should always return a String but doesn't
map.put(entry.getKey(), entry.getValue() == null ? "" : String.valueOf(entry.getValue()));
}
}
}
return map;
}
@NonNull
public int blockid() {
return blockid;
}
@NonNull
public String blockedby() {
if (blockedby != null)
return blockedby;
else return "";
}
@NonNull
public int blockedbyid() {
return blockedbyid;
}
@NonNull
public String blockreason() {
if (blockreason != null)
return blockreason;
else return "";
}
@NonNull
public String blocktimestamp() {
if (blocktimestamp != null)
return blocktimestamp;
else return "";
}
@NonNull
public String blockexpiry() {
if (blockexpiry != null)
return blockexpiry;
else return "";
}
}

View file

@ -0,0 +1,281 @@
package org.wikipedia.dataclient.mwapi.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.MwResponse;
import org.wikipedia.dataclient.page.PageLead;
import org.wikipedia.dataclient.page.PageLeadProperties;
import org.wikipedia.dataclient.page.Protection;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageProperties;
import org.wikipedia.page.PageTitle;
import org.wikipedia.page.Section;
import org.wikipedia.util.StringUtil;
import org.wikipedia.util.UriUtil;
import java.util.Collections;
import java.util.List;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
import static org.wikipedia.util.ImageUrlUtil.getUrlForSize;
/**
* Gson POJO for loading the first stage of page content.
*/
public class MwMobileViewPageLead extends MwResponse implements PageLead {
@SuppressWarnings("unused") private Mobileview mobileview;
/** Note: before using this check that #getMobileview != null */
@Override
public Page toPage(@NonNull PageTitle title) {
return new Page(adjustPageTitle(title, title.getPrefixedText()),
mobileview.getSections(),
mobileview.toPageProperties(),
false);
}
private PageTitle adjustPageTitle(@NonNull PageTitle title, @NonNull String originalPrefixedText) {
if (mobileview.getRedirected() != null) {
// Handle redirects properly.
title = new PageTitle(mobileview.getRedirected(), title.getWikiSite(),
title.getThumbUrl());
} else if (mobileview.getNormalizedTitle() != null) {
// We care about the normalized title only if we were not redirected
title = new PageTitle(mobileview.getNormalizedTitle(), title.getWikiSite(),
title.getThumbUrl());
}
if (mobileview.getDisplayTitle() != null
&& !StringUtil.removeHTMLTags(title.getDisplayText()).equals(StringUtil.removeHTMLTags(mobileview.getDisplayTitle()))) {
title = new PageTitle(StringUtil.removeHTMLTags(mobileview.getDisplayTitle()), title.getWikiSite(),
title.getThumbUrl());
}
if (mobileview.getDisplayTitle() != null
&& !mobileview.getDisplayTitle().equals(originalPrefixedText)
&& mobileview.getNormalizedTitle() == null) {
// Sometimes the MW api will not give us the "converted" or "redirected" title if switching between Chinese variants
// Ticket: https://phabricator.wikimedia.org/T206891#4672777
// We can the original prefixed title text (the one we used for calling API) to build the PageTitle
title = new PageTitle(originalPrefixedText, title.getWikiSite(), title.getThumbUrl());
}
if (mobileview.getRedirected() != null) {
title.setConvertedText(mobileview.getRedirected());
}
title.setDescription(mobileview.getDescription());
return title;
}
@Override @NonNull public String getLeadSectionContent() {
if (mobileview != null) {
return mobileview.getSections().get(0).getContent();
}
return "";
}
@Nullable
@Override
public String getTitlePronunciationUrl() {
return null;
}
@Nullable @Override public String getLeadImageUrl(int leadImageWidth) {
return mobileview == null ? null : mobileview.getLeadImageUrl(leadImageWidth);
}
@Nullable @Override public String getThumbUrl() {
return mobileview == null ? null : mobileview.getThumbUrl();
}
@Nullable @Override public String getDescription() {
return mobileview == null ? null : mobileview.getDescription();
}
@Nullable
@Override
public Location getGeo() {
return null;
}
@VisibleForTesting
public Mobileview getMobileview() {
return mobileview;
}
/**
* Almost everything is in this inner class.
*/
public static class Mobileview implements PageLeadProperties {
@SuppressWarnings("unused") private int id;
@SuppressWarnings("unused") private int namespace;
@SuppressWarnings("unused") private long revision;
@SuppressWarnings("unused") @Nullable private String lastmodified;
@SuppressWarnings("unused") @Nullable private String displaytitle;
@SuppressWarnings("unused") @Nullable private String redirected;
@SuppressWarnings("unused") @Nullable private String normalizedtitle;
@SuppressWarnings("unused") private int languagecount;
@SuppressWarnings("unused") private boolean editable;
@SuppressWarnings("unused") private boolean mainpage;
@SuppressWarnings("unused") private boolean disambiguation;
@SuppressWarnings("unused") @Nullable private String description;
@SuppressWarnings("unused") @Nullable private String descriptionsource;
@SuppressWarnings("unused") @SerializedName("image") @Nullable private PageImage pageImage;
@SuppressWarnings("unused") @SerializedName("thumb") @Nullable private PageImageThumb leadImage;
@SuppressWarnings("unused") @Nullable private Protection protection;
@SuppressWarnings("unused") @Nullable private List<Section> sections;
@SuppressWarnings("unused") @Nullable private MwQueryPage.PageProps pageprops;
/** Converter */
public PageProperties toPageProperties() {
return new PageProperties(this);
}
@Override
public int getId() {
return id;
}
@Override @NonNull public Namespace getNamespace() {
return Namespace.of(namespace);
}
@Override
public long getRevision() {
return revision;
}
@Override
@Nullable
public String getLastModified() {
return lastmodified;
}
@Override
public int getLanguageCount() {
return languagecount;
}
@Override
@Nullable
public String getDisplayTitle() {
return displaytitle;
}
@Override
@Nullable
public String getTitlePronunciationUrl() {
return null;
}
@Override
@Nullable
public Location getGeo() {
return null;
}
@Override
@Nullable
public String getRedirected() {
return redirected;
}
@Override
@Nullable
public String getNormalizedTitle() {
return normalizedtitle;
}
@Nullable
public String getDescription() {
return description;
}
@Override
@Nullable
public String getLeadImageUrl(int leadImageWidth) {
return leadImage != null ? leadImage.getUrl() : null;
}
@Override
@Nullable
public String getThumbUrl() {
return leadImage != null ? UriUtil.resolveProtocolRelativeUrl(getUrlForSize(leadImage.getUrl(), PREFERRED_THUMB_SIZE)) : null;
}
@Override
@Nullable
public String getLeadImageFileName() {
return pageImage != null ? pageImage.getFileName() : null;
}
@Override
@Nullable
public String getWikiBaseItem() {
return pageprops != null && pageprops.getWikiBaseItem() != null ? pageprops.getWikiBaseItem() : null;
}
@Override
@Nullable
public String getDescriptionSource() {
return descriptionsource;
}
@Override
@Nullable
public String getFirstAllowedEditorRole() {
return protection != null ? protection.getFirstAllowedEditorRole() : null;
}
@Override
public boolean isEditable() {
return editable;
}
@Override
public boolean isMainPage() {
return mainpage;
}
@Override
public boolean isDisambiguation() {
return disambiguation;
}
@Override @NonNull public List<Section> getSections() {
return sections == null ? Collections.emptyList() : sections;
}
}
/**
* For the lead image File: page name
*/
public static class PageImage {
@SuppressWarnings("unused") @SerializedName("file") private String fileName;
public String getFileName() {
return fileName;
}
}
/**
* For the lead image URL
*/
public static class PageImageThumb {
@SuppressWarnings("unused") private String url;
public String getUrl() {
return url;
}
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.dataclient.mwapi.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.page.PageRemaining;
import org.wikipedia.page.Section;
import java.util.Collections;
import java.util.List;
/**
* Gson POJO for loading remaining page content.
*/
public class MwMobileViewPageRemaining implements PageRemaining {
@SuppressWarnings("unused") @Nullable private MwMobileViewPageLead.Mobileview mobileview;
@NonNull @Override public List<Section> sections() {
return mobileview == null ? Collections.emptyList() : mobileview.getSections();
}
}

View file

@ -0,0 +1,54 @@
package org.wikipedia.dataclient.mwapi.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageClient;
import org.wikipedia.dataclient.page.PageSummary;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
/**
* Retrofit web service client for MediaWiki PHP API.
*/
public class MwPageClient implements PageClient {
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<? extends PageSummary> summary(@NonNull WikiSite wiki, @NonNull String title, @Nullable String referrerUrl) {
return ServiceFactory.get(wiki).getSummary(referrerUrl, title, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<MwMobileViewPageLead>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadImageWidth) {
return ServiceFactory.get(wiki).getLeadSection(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, referrerUrl, title, leadImageWidth, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<MwMobileViewPageRemaining>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.get(wiki).getRemainingSections(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.get(wiki).getRemainingSectionsUrl(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title, wiki.languageCode()).request();
}
}

View file

@ -0,0 +1,83 @@
package org.wikipedia.dataclient.mwapi.page;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.page.PageSummary;
import org.wikipedia.page.Namespace;
/**
* Useful for link previews coming from MW API.
*/
public class MwQueryPageSummary extends MwQueryResponse implements PageSummary {
@Override @Nullable public String getTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().title();
}
@Override @Nullable public String getDisplayTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return (query().firstPage().pageProps() != null && !TextUtils.isEmpty(query().firstPage().pageProps().getDisplayTitle()))
? query().firstPage().pageProps().getDisplayTitle() : query().firstPage().title();
}
@Override @Nullable public String getConvertedTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return (query().firstPage().convertedTo() != null && !TextUtils.isEmpty(query().firstPage().convertedTo()))
? query().firstPage().convertedTo() : query().firstPage().title();
}
@Override @Nullable
public String getExtract() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().extract();
}
@Override @Nullable
public String getExtractHtml() {
return getExtract();
}
@Override @Nullable
public String getThumbnailUrl() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().thumbUrl();
}
@Override @NonNull
public Namespace getNamespace() {
if (query() == null || query().firstPage() == null) {
return Namespace.MAIN;
}
return query().firstPage().namespace();
}
@NonNull @Override
public String getType() {
if (query() != null && query().firstPage() != null && query().firstPage().pageProps() != null
&& query().firstPage().pageProps().isDisambiguation()) {
return TYPE_DISAMBIGUATION;
}
return TYPE_STANDARD;
}
@Override public int getPageId() {
if (query() == null || query().firstPage() == null) {
return 0;
}
return query().firstPage().pageId();
}
}

View file

@ -0,0 +1,74 @@
package org.wikipedia.dataclient.okhttp;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
/**
* This is a subclass of InputStream that implements the available() method reliably enough
* to satisfy WebResourceResponses or other consumers like BufferedInputStream that depend
* on available() to return a meaningful value.
*
* The problem is that the InputStream provided by OkHttp's body().byteStream() returns zero
* when calling available() prior to making any read() calls, which means that it will break
* any consumers that wrap a BufferedInputStream onto this stream, or any other wrapper that
* relies on a consistent implementation of available().
*
* This is initialized with the original InputStream plus its total size, which must be known
* at the time of instantiation. You may then call the read() and skip() methods in the usual
* way, and then be able to call available() and get the number of bytes left to read.
*/
public class AvailableInputStream extends InputStream {
private InputStream stream;
private long available;
public AvailableInputStream(InputStream stream, long available) {
this.stream = stream;
this.available = available;
}
@Override public int read() throws IOException {
decreaseAvailable(1);
return stream.read();
}
@Override public int read(@NonNull byte[] b) throws IOException {
int ret = stream.read(b);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public int read(@NonNull byte[] b, int off, int len) throws IOException {
int ret = stream.read(b, off, len);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public long skip(long n) throws IOException {
long ret = stream.skip(n);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public int available() throws IOException {
int ret = stream.available();
if (ret == 0 && available > 0) {
return (int) available;
}
return ret;
}
private void decreaseAvailable(long n) {
available -= n;
if (available < 0) {
available = 0;
}
}
}

View file

@ -0,0 +1,54 @@
package org.wikipedia.dataclient.okhttp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.dataclient.restbase.RbServiceError;
import org.wikipedia.util.log.L;
import java.io.IOException;
import okhttp3.Response;
public class HttpStatusException extends IOException {
private final int code;
private final String url;
@Nullable private ServiceError serviceError;
public HttpStatusException(@NonNull Response rsp) {
this.code = rsp.code();
this.url = rsp.request().url().uri().toString();
try {
if (rsp.body() != null && rsp.body().contentType() != null
&& rsp.body().contentType().toString().contains("json")) {
serviceError = RbServiceError.create(rsp.body().string());
}
} catch (Exception e) {
L.e(e);
}
}
public HttpStatusException(@Nullable ServiceError error) {
serviceError = error;
code = 0;
url = "";
}
public int code() {
return code;
}
public ServiceError serviceError() {
return serviceError;
}
@Override
public String getMessage() {
String str = "Code: " + Integer.toString(code) + ", URL: " + url;
if (serviceError != null) {
str += ", title: " + serviceError.getTitle() + ", detail: " + serviceError.getDetails();
}
return str;
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.okhttp
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class TestStubInterceptor : Interceptor {
interface Callback {
@Throws(IOException::class)
fun getResponse(request: Interceptor.Chain): Response
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
return if (CALLBACK != null) {
CALLBACK!!.getResponse(chain)
} else chain.proceed(chain.request())
}
companion object {
var CALLBACK: Callback? = null
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.dataclient.okhttp
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class UnsuccessfulResponseInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val rsp = chain.proceed(chain.request())
if (rsp.isSuccessful) {
return rsp
}
throw HttpStatusException(rsp)
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.okhttp.util;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import okhttp3.HttpUrl;
public final class HttpUrlUtil {
private static final List<String> RESTBASE_SEGMENT_IDENTIFIERS = Arrays.asList("rest_v1", "v1");
public static boolean isRestBase(@NonNull HttpUrl url) {
return !Collections.disjoint(url.encodedPathSegments(), RESTBASE_SEGMENT_IDENTIFIERS);
}
public static boolean isMobileView(@NonNull HttpUrl url) {
return "mobileview".equals(url.queryParameter("action"));
}
private HttpUrlUtil() { }
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
/**
* Generic interface for Page content service.
* Usually we would use direct Retrofit Callbacks here but since we have two ways of
* getting to the data (MW API and RESTBase) we add this layer of indirection -- until we drop one.
*/
public interface PageClient {
/**
* Gets a page summary for a given title -- for link previews
*
* @param title the page title to be used including prefix
*/
@NonNull <T extends PageSummary> Observable<T> summary(@NonNull WikiSite wiki,
@NonNull String title,
@Nullable String referrerUrl);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @param leadThumbnailWidth one of the bucket widths for the lead image
*/
@NonNull <T extends PageLead> Observable<Response<T>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadThumbnailWidth);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@NonNull <T extends PageRemaining> Observable<Response<T>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title);
/**
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@NonNull Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title);
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.dataclient.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageTitle;
/**
* Gson POJI for loading the first stage of page content.
*/
public interface PageLead {
/** Note: before using this check that #hasError is false */
Page toPage(PageTitle title);
@NonNull String getLeadSectionContent();
@Nullable String getTitlePronunciationUrl();
@Nullable String getLeadImageUrl(int leadImageWidth);
@Nullable String getThumbUrl();
@Nullable String getDescription();
@Nullable Location getGeo();
}

View file

@ -0,0 +1,73 @@
package org.wikipedia.dataclient.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Section;
import java.util.List;
/**
* The main properties of a page
*/
public interface PageLeadProperties {
int getId();
@NonNull Namespace getNamespace();
long getRevision();
@Nullable
String getLastModified();
int getLanguageCount();
@Nullable
String getDisplayTitle();
@Nullable
String getTitlePronunciationUrl();
@Nullable
Location getGeo();
@Nullable
String getRedirected();
@Nullable
String getNormalizedTitle();
@Nullable
String getWikiBaseItem();
@Nullable
String getDescriptionSource();
/**
* @return Nullable URL with no scheme. For example, foo.bar.com/ instead of
* http://foo.bar.com/.
*/
@Nullable
String getLeadImageUrl(int leadImageWidth);
@Nullable
String getThumbUrl();
@Nullable
String getLeadImageFileName();
@Nullable
String getFirstAllowedEditorRole();
boolean isEditable();
boolean isMainPage();
boolean isDisambiguation();
@NonNull List<Section> getSections();
}

View file

@ -0,0 +1,14 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import org.wikipedia.page.Section;
import java.util.List;
/**
* Gson POJI for loading remaining page content.
*/
public interface PageRemaining {
@NonNull List<Section> sections();
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Namespace;
/**
* Represents a summary of a page, useful for page previews.
*/
public interface PageSummary {
String TYPE_STANDARD = "standard";
String TYPE_DISAMBIGUATION = "disambiguation";
String TYPE_MAIN_PAGE = "mainpage";
String TYPE_NO_EXTRACT = "no-extract";
@NonNull String getType();
@Nullable String getTitle();
@Nullable String getDisplayTitle();
@Nullable String getConvertedTitle();
@Nullable String getExtract();
@Nullable String getExtractHtml();
@Nullable String getThumbnailUrl();
@NonNull Namespace getNamespace();
int getPageId();
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
/** Protection settings for a page */
public class Protection {
@SuppressWarnings("MismatchedReadAndWriteOfArray") @NonNull private Set<String> edit = Collections.emptySet();
// TODO should send them all, but callers need to be updated, too, (future patch)
@Nullable
public String getFirstAllowedEditorRole() {
return edit.isEmpty() ? null : edit.iterator().next();
}
@NonNull
public Set<String> getEditRoles() {
return Collections.unmodifiableSet(edit);
}
}

View file

@ -0,0 +1,56 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.json.annotations.Required;
import java.util.Map;
public class RbDefinition {
@Required @NonNull private Map<String, Usage[]> usagesByLang;
public RbDefinition(@NonNull Map<String, RbDefinition.Usage[]> usages) {
usagesByLang = usages;
}
@Nullable public Usage[] getUsagesForLang(String langCode) {
return usagesByLang.get(langCode);
}
public static class Usage {
@Required @NonNull private String partOfSpeech;
@Required @NonNull private Definition[] definitions;
public Usage(@NonNull String partOfSpeech, @NonNull Definition[] definitions) {
this.partOfSpeech = partOfSpeech;
this.definitions = definitions;
}
@NonNull public String getPartOfSpeech() {
return partOfSpeech;
}
@NonNull public Definition[] getDefinitions() {
return definitions;
}
}
public static class Definition {
@Required @NonNull private String definition;
@Nullable private String[] examples;
public Definition(@NonNull String definition, @Nullable String[] examples) {
this.definition = definition;
this.examples = examples;
}
@NonNull public String getDefinition() {
return definition;
}
@Nullable public String[] getExamples() {
return examples;
}
}
}

View file

@ -0,0 +1,33 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import java.util.ArrayList;
import java.util.List;
public class RbRelatedPages {
@SuppressWarnings("unused") @Nullable private List<RbPageSummary> pages;
@Nullable
public List<RbPageSummary> getPages() {
return pages;
}
@NonNull
public List<RbPageSummary> getPages(int limit) {
List<RbPageSummary> list = new ArrayList<>();
if (getPages() != null) {
for (RbPageSummary page : getPages()) {
list.add(page);
if (limit == list.size()) {
break;
}
}
}
return list;
}
}

View file

@ -0,0 +1,35 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.json.GsonUnmarshaller;
import org.wikipedia.model.BaseModel;
/**
* Gson POJO for a RESTBase API error.
*/
public class RbServiceError extends BaseModel implements ServiceError {
@SuppressWarnings("unused") private String type;
@SuppressWarnings("unused") private String title;
@SuppressWarnings("unused") private String detail;
@SuppressWarnings("unused") private String method;
@SuppressWarnings("unused") private String uri;
public static RbServiceError create(@NonNull String rspBody) {
return GsonUnmarshaller.unmarshal(RbServiceError.class, rspBody);
}
@Override
@NonNull
public String getTitle() {
return StringUtils.defaultString(title);
}
@Override
@NonNull
public String getDetails() {
return StringUtils.defaultString(detail);
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.restbase.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageClient;
import org.wikipedia.dataclient.page.PageSummary;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
// todo: consolidate with MwPageClient or just use the Services directly!
/**
* Retrofit web service client for RESTBase Nodejs API.
*/
public class RbPageClient implements PageClient {
// todo: RbPageSummary should specify an @Required annotation that throws a JsonParseException
// when the body is null rather than requiring all clients to check for a null body. There
// may be some abandoned demo patches that already have this functionality. It should be
// part of the Gson augmentation package and eventually cut into a separate lib. Repeat
// everywhere a Response.body() == null check occurs that throws
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<? extends PageSummary> summary(@NonNull WikiSite wiki, @NonNull String title, @Nullable String referrerUrl) {
return ServiceFactory.getRest(wiki).getSummary(referrerUrl, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<RbPageLead>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadThumbnailWidth) {
return ServiceFactory.getRest(wiki).getLeadSection(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, referrerUrl, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<RbPageRemaining>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.getRest(wiki).getRemainingSections(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.getRest(wiki).getRemainingSectionsUrl(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title).request();
}
}

View file

@ -0,0 +1,268 @@
package org.wikipedia.dataclient.restbase.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.page.PageLead;
import org.wikipedia.dataclient.page.PageLeadProperties;
import org.wikipedia.dataclient.page.Protection;
import org.wikipedia.page.GeoTypeAdapter;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageProperties;
import org.wikipedia.page.PageTitle;
import org.wikipedia.page.Section;
import org.wikipedia.util.UriUtil;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
/**
* Gson POJO for loading the first stage of page content.
*/
@SuppressWarnings("unused")
public class RbPageLead implements PageLead, PageLeadProperties {
private int ns;
private int id;
private long revision;
@Nullable private String lastmodified;
@Nullable private String displaytitle;
@Nullable private String redirected;
@Nullable private String normalizedtitle;
@Nullable @SerializedName("wikibase_item") private String wikiBaseItem;
@Nullable @SerializedName("pronunciation") private TitlePronunciation titlePronunciation;
@Nullable @JsonAdapter(GeoTypeAdapter.class) private Location geo;
private int languagecount;
private boolean editable;
private boolean mainpage;
private boolean disambiguation;
@Nullable private String description;
@Nullable @SerializedName("description_source") private String descriptionSource;
@Nullable private Image image;
@Nullable private Protection protection;
@Nullable private List<Section> sections;
/** Note: before using this check that #getMobileview != null */
@Override
public Page toPage(PageTitle title) {
return new Page(adjustPageTitle(title),
getSections(),
toPageProperties(),
true);
}
PageTitle adjustPageTitle(PageTitle title) {
if (redirected != null) {
// Handle redirects properly.
title = new PageTitle(redirected, title.getWikiSite(), title.getThumbUrl());
} else if (normalizedtitle != null) {
// We care about the normalized title only if we were not redirected
title = new PageTitle(normalizedtitle, title.getWikiSite(), title.getThumbUrl());
}
title.setDescription(description);
return title;
}
@Override
public String getLeadSectionContent() {
if (sections != null) {
return sections.get(0).getContent();
} else {
return "";
}
}
/** Converter */
private PageProperties toPageProperties() {
return new PageProperties(this);
}
@Override
public int getId() {
return id;
}
@NonNull @Override public Namespace getNamespace() {
return Namespace.of(ns);
}
@Override
public long getRevision() {
return revision;
}
@Override
@Nullable
public String getLastModified() {
return lastmodified;
}
@Override
@Nullable
public String getTitlePronunciationUrl() {
return titlePronunciation == null
? null
: UriUtil.resolveProtocolRelativeUrl(titlePronunciation.getUrl());
}
@Override
@Nullable
public Location getGeo() {
return geo;
}
@Override
public int getLanguageCount() {
return languagecount;
}
@Override
@Nullable
public String getDisplayTitle() {
return displaytitle;
}
@Override
@Nullable
public String getRedirected() {
return redirected;
}
@Override
@Nullable
public String getNormalizedTitle() {
return normalizedtitle;
}
@Override
@Nullable
public String getWikiBaseItem() {
return wikiBaseItem;
}
@Override
@Nullable
public String getDescription() {
return description;
}
@Override
@Nullable
public String getDescriptionSource() {
return descriptionSource;
}
@Override
@Nullable
public String getLeadImageUrl(int leadImageWidth) {
return image != null ? image.getUrl(leadImageWidth) : null;
}
@Override
@Nullable
public String getThumbUrl() {
return image != null ? image.getUrl(PREFERRED_THUMB_SIZE) : null;
}
@Override
@Nullable
public String getLeadImageFileName() {
return image != null ? image.getFileName() : null;
}
@Override
@Nullable
public String getFirstAllowedEditorRole() {
return protection != null ? protection.getFirstAllowedEditorRole() : null;
}
@Override
public boolean isEditable() {
return editable;
}
public Set<String> getEditRoles() {
return protection != null ? protection.getEditRoles() : Collections.emptySet();
}
@Override
public boolean isMainPage() {
return mainpage;
}
@Override
public boolean isDisambiguation() {
return disambiguation;
}
@Override @NonNull public List<Section> getSections() {
return sections == null ? Collections.emptyList() : sections;
}
/**
* For the lead image File: page name
*/
public static class TitlePronunciation {
@SuppressWarnings("unused,NullableProblems") @NonNull private String url;
@NonNull
public String getUrl() {
return url;
}
}
/**
* For the lead image File: page name
*/
public static class Image {
@SuppressWarnings("unused") @SerializedName("file") private String fileName;
@SuppressWarnings("unused") private ThumbUrls urls;
public String getFileName() {
return fileName;
}
@Nullable
public String getUrl(int width) {
return urls != null ? urls.get(width) : null;
}
}
/**
* For the lead image URLs
*/
public static class ThumbUrls {
private static final int SMALL = 320;
private static final int MEDIUM = 640;
private static final int LARGE = 800;
private static final int XL = 1024;
@SuppressWarnings("unused") @SerializedName("320") private String small;
@SuppressWarnings("unused") @SerializedName("640") private String medium;
@SuppressWarnings("unused") @SerializedName("800") private String large;
@SuppressWarnings("unused") @SerializedName("1024") private String xl;
@Nullable
public String get(int width) {
switch (width) {
case SMALL:
return small;
case MEDIUM:
return medium;
case LARGE:
return large;
case XL:
return xl;
default:
return null;
}
}
}
}

View file

@ -0,0 +1,24 @@
package org.wikipedia.dataclient.restbase.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.page.PageRemaining;
import org.wikipedia.page.Section;
import java.util.Collections;
import java.util.List;
/**
* Gson POJO for loading remaining page content.
*/
public class RbPageRemaining implements PageRemaining {
@SuppressWarnings("unused") @Nullable private List<Section> sections;
@NonNull @Override public List<Section> sections() {
if (sections == null) {
return Collections.emptyList();
}
return sections;
}
}

View file

@ -0,0 +1,136 @@
package org.wikipedia.dataclient.restbase.page;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageSummary;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.PageTitle;
/**
* A standardized page summary object constructed by RESTBase, used for link previews and as the
* base class for various feed content (see the FeedPageSummary class).
*
* N.B.: The "title" field here sent by RESTBase is the *normalized* page title. However, in the
* FeedPageSummary subclass, "title" becomes the un-normalized, raw title, and the normalized title
* is sent as "normalizedtitle".
*/
@SuppressWarnings("unused")
public class RbPageSummary implements PageSummary {
@Nullable private String type;
@SuppressWarnings("NullableProblems") @Required @NonNull private String title;
@Nullable private String normalizedtitle;
@SuppressWarnings("NullableProblems") @NonNull private String displaytitle;
@Nullable private NamespaceContainer namespace;
@Nullable private String extract;
@Nullable @SerializedName("extract_html") private String extractHtml;
@Nullable private String description;
@Nullable private Thumbnail thumbnail;
@Nullable @SerializedName("originalimage") private Thumbnail originalImage;
@Nullable private String lang;
private int pageid;
@Nullable @SerializedName("wikibase_item") private String wikiBaseItem;
@Override @NonNull
public String getTitle() {
return title;
}
@Override @NonNull
public String getDisplayTitle() {
return displaytitle;
}
@Override @NonNull
public String getConvertedTitle() {
return title;
}
@Override @NonNull
public Namespace getNamespace() {
return namespace == null ? Namespace.MAIN : Namespace.of(namespace.id());
}
@Override @NonNull
public String getType() {
return TextUtils.isEmpty(type) ? TYPE_STANDARD : type;
}
@Override @Nullable
public String getExtract() {
return extract;
}
@Override @Nullable
public String getExtractHtml() {
return extractHtml;
}
@Override @Nullable
public String getThumbnailUrl() {
return thumbnail == null ? null : thumbnail.getUrl();
}
@Nullable
public String getDescription() {
return description;
}
@NonNull
public String getNormalizedTitle() {
return normalizedtitle == null ? title : normalizedtitle;
}
@Nullable
public String getOriginalImageUrl() {
return originalImage == null ? null : originalImage.getUrl();
}
@Nullable
public String getWikiBaseItem() {
return wikiBaseItem;
}
@NonNull
public PageTitle getPageTitle(@NonNull WikiSite wiki) {
return new PageTitle(getTitle(), wiki, getThumbnailUrl(), getDescription());
}
public int getPageId() {
return pageid;
}
public String getLang() {
return lang;
}
/**
* For the thumbnail URL of the page
*/
private static class Thumbnail {
@SuppressWarnings("unused") private String source;
public String getUrl() {
return source;
}
}
private static class NamespaceContainer {
@SuppressWarnings("unused") private int id;
@SuppressWarnings("unused") @Nullable private String text;
public int id() {
return id;
}
}
@Override public String toString() {
return getTitle();
}
}

View file

@ -0,0 +1,75 @@
package org.wikipedia.dataclient.retrofit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import retrofit2.Response;
/**
* This is RetrofitError converted to Retrofit 2
*/
public class RetrofitException extends RuntimeException {
public static RetrofitException httpError(Response<?> response) {
return httpError(response.raw().request().url().toString(), response);
}
public static RetrofitException httpError(String url, Response<?> response) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, url, response.code(), Kind.HTTP, null);
}
public static RetrofitException httpError(@NonNull okhttp3.Response response) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, response.request().url().toString(), response.code(), Kind.HTTP,
null);
}
public static RetrofitException networkError(IOException exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.NETWORK, exception);
}
public static RetrofitException unexpectedError(Throwable exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.UNEXPECTED, exception);
}
/** Identifies the event kind which triggered a {@link RetrofitException}. */
public enum Kind {
/** An {@link IOException} occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
private final String url;
@Nullable private final Integer code;
private final Kind kind;
RetrofitException(String message, String url, @Nullable Integer code, Kind kind, Throwable exception) {
super(message, exception);
this.url = url;
this.code = code;
this.kind = kind;
}
/** The request URL which produced the error. */
public String getUrl() {
return url;
}
/** HTTP status code. */
@Nullable public Integer getCode() {
return code;
}
/** The event kind which triggered this error. */
public Kind getKind() {
return kind;
}
}

View file

@ -0,0 +1,79 @@
package org.wikipedia.edit;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
public class Edit extends MwPostResponse {
@SuppressWarnings("unused,") @Nullable private Result edit;
@Nullable public Result edit() {
return edit;
}
boolean hasEditResult() {
return edit != null;
}
public class Result {
@SuppressWarnings("unused") @Nullable private String result;
@SuppressWarnings("unused") private int newrevid;
@SuppressWarnings("unused") @Nullable private Captcha captcha;
@SuppressWarnings("unused") @Nullable private String code;
@SuppressWarnings("unused") @Nullable private String info;
@SuppressWarnings("unused") @Nullable private String warning;
@SuppressWarnings("unused") @Nullable private String spamblacklist;
@Nullable String status() {
return result;
}
public int newRevId() {
return newrevid;
}
public boolean editSucceeded() {
return "Success".equals(result);
}
@Nullable String captchaId() {
return captcha == null ? null : captcha.id();
}
public boolean hasEditErrorCode() {
return code != null;
}
boolean hasCaptchaResponse() {
return captcha != null;
}
@Nullable public String code() {
return code;
}
@Nullable public String info() {
return info;
}
@Nullable public String warning() {
return warning;
}
@Nullable String spamblacklist() {
return spamblacklist;
}
boolean hasSpamBlacklistResponse() {
return spamblacklist != null;
}
}
private static class Captcha {
@SuppressWarnings("unused") @Nullable private String id;
@Nullable String id() {
return id;
}
}
}

View file

@ -0,0 +1,79 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
public class EditAbuseFilterResult extends EditResult {
static final int TYPE_WARNING = 1;
static final int TYPE_ERROR = 2;
@Nullable private final String code;
@Nullable private final String info;
@Nullable private final String warning;
EditAbuseFilterResult(@Nullable String code, @Nullable String info, @Nullable String warning) {
super("Failure");
this.code = code;
this.info = info;
this.warning = warning;
}
private EditAbuseFilterResult(Parcel in) {
super(in);
code = in.readString();
info = in.readString();
warning = in.readString();
}
@Nullable public String getCode() {
return code;
}
@Nullable public String getInfo() {
return info;
}
@Nullable public String getWarning() {
return warning;
}
@Override public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(code);
dest.writeString(info);
dest.writeString(warning);
}
public int getType() {
if (code != null && code.startsWith("abusefilter-warning")) {
return TYPE_WARNING;
} else if (code != null && code.startsWith("abusefilter-disallowed")) {
return TYPE_ERROR;
} else if (info != null && info.startsWith("Hit AbuseFilter")) {
// This case is here because, unfortunately, an admin can create an abuse filter which
// emits an arbitrary error code over the API.
// TODO: More properly handle the case where the AbuseFilter throws an arbitrary error.
// Oh, and, you know, also fix the AbuseFilter API to not throw arbitrary error codes.
return TYPE_ERROR;
} else {
// We have no understanding of what kind of abuse filter response we got. It's safest
// to simply treat these as an error.
return TYPE_ERROR;
}
}
public static final Parcelable.Creator<EditAbuseFilterResult> CREATOR
= new Parcelable.Creator<EditAbuseFilterResult>() {
@Override
public EditAbuseFilterResult createFromParcel(Parcel in) {
return new EditAbuseFilterResult(in);
}
@Override
public EditAbuseFilterResult[] newArray(int size) {
return new EditAbuseFilterResult[size];
}
};
}

View file

@ -0,0 +1,72 @@
package org.wikipedia.edit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.captcha.CaptchaResult;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Response;
public class EditClient {
public interface Callback {
void success(@NonNull Call<Edit> call, @NonNull EditResult result);
void failure(@NonNull Call<Edit> call, @NonNull Throwable caught);
}
@SuppressWarnings("checkstyle:parameternumber")
public Call<Edit> request(@NonNull WikiSite wiki, @NonNull PageTitle title, int section,
@NonNull String text, @NonNull String token, @NonNull String summary,
@Nullable String baseTimeStamp, boolean loggedIn, @Nullable String captchaId,
@Nullable String captchaWord, @NonNull Callback cb) {
return request(ServiceFactory.get(wiki), title, section, text, token, summary,
baseTimeStamp, loggedIn, captchaId, captchaWord, cb);
}
@VisibleForTesting @SuppressWarnings("checkstyle:parameternumber")
Call<Edit> request(@NonNull Service service, @NonNull PageTitle title, int section,
@NonNull String text, @NonNull String token, @NonNull String summary,
@Nullable String baseTimeStamp, boolean loggedIn, @Nullable String captchaId,
@Nullable String captchaWord, @NonNull final Callback cb) {
Call<Edit> call = service.postEditSubmit(title.getPrefixedText(), section, summary, loggedIn ? "user" : null,
text, baseTimeStamp, token, captchaId, captchaWord);
call.enqueue(new retrofit2.Callback<Edit>() {
@Override
public void onResponse(@NonNull Call<Edit> call, @NonNull Response<Edit> response) {
if (response.body().hasEditResult()) {
handleEditResult(response.body().edit(), call, cb);
} else {
cb.failure(call, new IOException("An unknown error occurred."));
}
}
@Override
public void onFailure(@NonNull Call<Edit> call, @NonNull Throwable t) {
cb.failure(call, t);
}
});
return call;
}
private void handleEditResult(@NonNull Edit.Result result, @NonNull Call<Edit> call,
@NonNull Callback cb) {
if (result.editSucceeded()) {
cb.success(call, new EditSuccessResult(result.newRevId()));
} else if (result.hasEditErrorCode()) {
cb.success(call, new EditAbuseFilterResult(result.code(), result.info(), result.warning()));
} else if (result.hasSpamBlacklistResponse()) {
cb.success(call, new EditSpamBlacklistResult(result.spamblacklist()));
} else if (result.hasCaptchaResponse()) {
cb.success(call, new CaptchaResult(result.captchaId()));
} else {
cb.failure(call, new IOException("Received unrecognized edit response"));
}
}
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
import org.wikipedia.model.BaseModel;
public abstract class EditResult extends BaseModel implements Parcelable {
private final String result;
public EditResult(String result) {
this.result = result;
}
protected EditResult(Parcel in) {
this.result = in.readString();
}
public String getResult() {
return result;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(result);
}
}

View file

@ -0,0 +1,40 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
public class EditSpamBlacklistResult extends EditResult {
private final String domain;
public EditSpamBlacklistResult(String domain) {
super("Failure");
this.domain = domain;
}
protected EditSpamBlacklistResult(Parcel in) {
super(in);
domain = in.readString();
}
public String getDomain() {
return domain;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(domain);
}
public static final Parcelable.Creator<EditSpamBlacklistResult> CREATOR
= new Parcelable.Creator<EditSpamBlacklistResult>() {
@Override
public EditSpamBlacklistResult createFromParcel(Parcel in) {
return new EditSpamBlacklistResult(in);
}
@Override
public EditSpamBlacklistResult[] newArray(int size) {
return new EditSpamBlacklistResult[size];
}
};
}

View file

@ -0,0 +1,34 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
public class EditSuccessResult extends EditResult {
private final int revID;
public EditSuccessResult(int revID) {
super("Success");
this.revID = revID;
}
private EditSuccessResult(Parcel in) {
super(in);
revID = in.readInt();
}
public int getRevID() {
return revID;
}
public static final Parcelable.Creator<EditSuccessResult> CREATOR
= new Parcelable.Creator<EditSuccessResult>() {
@Override
public EditSuccessResult createFromParcel(Parcel in) {
return new EditSuccessResult(in);
}
@Override
public EditSuccessResult[] newArray(int size) {
return new EditSuccessResult[size];
}
};
}

View file

@ -0,0 +1,29 @@
package org.wikipedia.edit.preview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
public class EditPreview extends MwPostResponse {
@SuppressWarnings("unused") @Nullable private Parse parse;
boolean hasPreviewResult() {
return parse != null;
}
@Nullable String result() {
return parse != null ? parse.text() : null;
}
private static class Parse {
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@SuppressWarnings("unused") @SerializedName("pageid") private int pageId;
@SuppressWarnings("unused,NullableProblems") @NonNull private String text;
@NonNull String text() {
return text;
}
}
}

View file

@ -0,0 +1,46 @@
package org.wikipedia.feed.aggregated;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.feed.image.FeaturedImage;
import org.wikipedia.feed.mostread.MostReadArticles;
import org.wikipedia.feed.news.NewsItem;
import org.wikipedia.feed.onthisday.OnThisDay;
import java.util.List;
public class AggregatedFeedContent {
@SuppressWarnings("unused") @Nullable private RbPageSummary tfa;
@SuppressWarnings("unused") @Nullable private List<NewsItem> news;
@SuppressWarnings("unused") @SerializedName("mostread") @Nullable private MostReadArticles mostRead;
@SuppressWarnings("unused") @Nullable private FeaturedImage image;
@SuppressWarnings("unused") @Nullable private List<OnThisDay.Event> onthisday;
@Nullable
public List<OnThisDay.Event> onthisday() {
return onthisday;
}
@Nullable
public RbPageSummary tfa() {
return tfa;
}
@Nullable
List<NewsItem> news() {
return news;
}
@Nullable
MostReadArticles mostRead() {
return mostRead;
}
@Nullable
FeaturedImage potd() {
return image;
}
}

View file

@ -0,0 +1,164 @@
package org.wikipedia.feed.announcement;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.model.BaseModel;
import org.wikipedia.util.DateUtil;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class Announcement extends BaseModel {
public static final String SURVEY = "survey";
public static final String FUNDRAISING = "fundraising";
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String id;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String type;
@SuppressWarnings("unused,NullableProblems") @SerializedName("start_time") @Required @NonNull private String startTime;
@SuppressWarnings("unused,NullableProblems") @SerializedName("end_time") @Required @NonNull private String endTime;
@SuppressWarnings("unused") @NonNull private List<String> platforms = Collections.emptyList();
@SuppressWarnings("unused") @NonNull private List<String> countries = Collections.emptyList();
@SuppressWarnings("unused") @SerializedName("caption_HTML") @Nullable private String footerCaption;
@SuppressWarnings("unused") @SerializedName("image_url") @Nullable private String imageUrl;
@SuppressWarnings("unused") @SerializedName("image_height") @Nullable private String imageHeight;
@SuppressWarnings("unused") @SerializedName("logged_in") @Nullable private Boolean loggedIn;
@SuppressWarnings("unused") @SerializedName("reading_list_sync_enabled") @Nullable private Boolean readingListSyncEnabled;
@SuppressWarnings("unused") @Nullable private Boolean beta;
@SuppressWarnings("unused") @SerializedName("min_version") @Nullable private String minVersion;
@SuppressWarnings("unused") @SerializedName("max_version") @Nullable private String maxVersion;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String text;
@SuppressWarnings("unused") @Nullable private Action action;
@SuppressWarnings("unused") @SerializedName("negative_text") @Nullable private String negativeText;
public Announcement() { }
public Announcement(@NonNull String id, @NonNull String text, @NonNull String imageUrl,
@NonNull Action action, @NonNull String negativeText) {
this.id = id;
this.text = text;
this.imageUrl = imageUrl;
this.action = action;
this.negativeText = negativeText;
}
@NonNull String id() {
return id;
}
@NonNull String type() {
return type;
}
@Nullable Date startTime() {
try {
return DateUtil.iso8601DateParse(startTime);
} catch (ParseException e) {
return null;
}
}
@Nullable Date endTime() {
try {
return DateUtil.iso8601DateParse(endTime);
} catch (ParseException e) {
return null;
}
}
@NonNull List<String> platforms() {
return platforms;
}
@NonNull List<String> countries() {
return countries;
}
@NonNull String text() {
return text;
}
boolean hasAction() {
return action != null;
}
@NonNull String actionTitle() {
return action != null ? action.title() : "";
}
@NonNull String actionUrl() {
return action != null ? action.url() : "";
}
boolean hasFooterCaption() {
return !TextUtils.isEmpty(footerCaption);
}
@NonNull String footerCaption() {
return defaultString(footerCaption);
}
boolean hasImageUrl() {
return !TextUtils.isEmpty(imageUrl);
}
@NonNull String imageUrl() {
return defaultString(imageUrl);
}
@NonNull String imageHeight() {
return defaultString(imageHeight);
}
@Nullable String negativeText() {
return negativeText;
}
@Nullable Boolean loggedIn() {
return loggedIn;
}
@Nullable Boolean readingListSyncEnabled() {
return readingListSyncEnabled;
}
@Nullable Boolean beta() {
return beta;
}
@Nullable String minVersion() {
return minVersion;
}
@Nullable String maxVersion() {
return maxVersion;
}
public static class Action {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String title;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String url;
public Action(@NonNull String title, @NonNull String url) {
this.title = title;
this.url = url;
}
@NonNull String title() {
return title;
}
@NonNull String url() {
return url;
}
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.feed.announcement;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.model.BaseModel;
import java.util.Collections;
import java.util.List;
public class AnnouncementList extends BaseModel {
@SuppressWarnings("unused") @SerializedName("announce") @NonNull private List<Announcement> items = Collections.emptyList();
@NonNull
List<Announcement> items() {
return items;
}
}

View file

@ -0,0 +1,41 @@
package org.wikipedia.feed.announcement;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GeoIPCookie {
@NonNull private final String country;
@NonNull private final String region;
@NonNull private final String city;
@Nullable private final Location location;
GeoIPCookie(@NonNull String country, @NonNull String region, @NonNull String city, @Nullable Location location) {
this.country = country;
this.region = region;
this.city = city;
this.location = location;
}
@NonNull
public String country() {
return country;
}
@NonNull
public String region() {
return region;
}
@NonNull
public String city() {
return city;
}
@Nullable
public Location location() {
return location;
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.feed.announcement;
import android.location.Location;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
/*
This currently supports the "v4" version of the GeoIP cookie.
For some info about the format and contents of the cookie:
https://phabricator.wikimedia.org/diffusion/ECNO/browse/master/resources/subscribing/ext.centralNotice.geoIP.js
*/
public final class GeoIPCookieUnmarshaller {
private static final String COOKIE_NAME = "GeoIP";
private enum Component {
COUNTRY, REGION, CITY, LATITUDE, LONGITUDE, VERSION
}
@NonNull
public static GeoIPCookie unmarshal() {
return unmarshal(SharedPreferenceCookieManager.getInstance().getCookieByName(COOKIE_NAME));
}
@VisibleForTesting
@NonNull
static GeoIPCookie unmarshal(@Nullable String cookie) throws IllegalArgumentException {
if (TextUtils.isEmpty(cookie)) {
throw new IllegalArgumentException("Cookie is empty.");
}
String[] components = cookie.split(":");
if (components.length < Component.values().length) {
throw new IllegalArgumentException("Cookie is malformed.");
} else if (!components[Component.VERSION.ordinal()].equals("v4")) {
throw new IllegalArgumentException("Incorrect cookie version.");
}
Location location = null;
if (!TextUtils.isEmpty(components[Component.LATITUDE.ordinal()])
&& !TextUtils.isEmpty(components[Component.LONGITUDE.ordinal()])) {
location = new Location("");
try {
location.setLatitude(Double.parseDouble(components[Component.LATITUDE.ordinal()]));
location.setLongitude(Double.parseDouble(components[Component.LONGITUDE.ordinal()]));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Location is malformed.");
}
}
return new GeoIPCookie(components[Component.COUNTRY.ordinal()],
components[Component.REGION.ordinal()],
components[Component.CITY.ordinal()],
location);
}
private GeoIPCookieUnmarshaller() {
}
}

View file

@ -0,0 +1,37 @@
package org.wikipedia.feed.configure;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("unused")
public class FeedAvailability {
@SerializedName("todays_featured_article") private List<String> featuredArticle;
@SerializedName("most_read") private List<String> mostRead;
@SerializedName("picture_of_the_day") private List<String> featuredPicture;
@SerializedName("in_the_news") private List<String> news;
@SerializedName("on_this_day") private List<String> onThisDay;
@NonNull public List<String> featuredArticle() {
return featuredArticle != null ? featuredArticle : Collections.emptyList();
}
@NonNull public List<String> mostRead() {
return mostRead != null ? mostRead : Collections.emptyList();
}
@NonNull public List<String> featuredPicture() {
return featuredPicture != null ? featuredPicture : Collections.emptyList();
}
@NonNull public List<String> news() {
return news != null ? news : Collections.emptyList();
}
@NonNull public List<String> onThisDay() {
return onThisDay != null ? onThisDay : Collections.emptyList();
}
}

View file

@ -0,0 +1,34 @@
package org.wikipedia.feed.image;
import androidx.annotation.NonNull;
import org.wikipedia.gallery.GalleryItem;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.json.annotations.Required;
public final class FeaturedImage extends GalleryItem implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String title;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private ImageInfo image;
private int age;
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
@NonNull
public String title() {
return title;
}
@Override
public void postProcess() {
setTitle(title);
getOriginal().setSource(image.getSource());
}
}

View file

@ -0,0 +1,49 @@
package org.wikipedia.feed.model;
import androidx.annotation.NonNull;
import java.util.Calendar;
import static java.util.TimeZone.getTimeZone;
public class UtcDate {
@NonNull private Calendar cal;
@NonNull private String year;
@NonNull private String month;
@NonNull private String date;
public UtcDate(int age) {
this.cal = Calendar.getInstance(getTimeZone("UTC"));
cal.add(Calendar.DATE, -age);
this.year = Integer.toString(cal.get(Calendar.YEAR));
this.month = pad(Integer.toString(cal.get(Calendar.MONTH) + 1));
this.date = pad(Integer.toString(cal.get(Calendar.DATE)));
}
@NonNull
public Calendar baseCalendar() {
return cal;
}
@NonNull
public String year() {
return year;
}
@NonNull
public String month() {
return month;
}
@NonNull
public String date() {
return date;
}
private String pad(String value) {
if (value.length() == 1) {
return "0" + value;
}
return value;
}
}

View file

@ -0,0 +1,22 @@
package org.wikipedia.feed.mostread;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import java.util.Date;
import java.util.List;
public final class MostReadArticles {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private Date date;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private List<RbPageSummary> articles;
@NonNull public Date date() {
return date;
}
@NonNull public List<RbPageSummary> articles() {
return articles;
}
}

View file

@ -0,0 +1,56 @@
package org.wikipedia.feed.news;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import java.util.Collections;
import java.util.List;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
import static org.wikipedia.util.ImageUrlUtil.getUrlForSize;
public final class NewsItem {
@SuppressWarnings("unused") @Required @Nullable private String story;
@SuppressWarnings("unused") @Nullable private List<RbPageSummary> links
= Collections.emptyList();
@NonNull String story() {
return StringUtils.defaultString(story);
}
@NonNull public List<RbPageSummary> links() {
return links != null ? links : Collections.emptyList();
}
@Nullable public Uri thumb() {
Uri uri = getFirstImageUri(links());
return uri != null ? getUrlForSize(uri, PREFERRED_THUMB_SIZE) : null;
}
@Nullable Uri featureImage() {
return getFirstImageUri(links());
}
/**
* Iterate through the CardPageItems associated with the news story's links and return the first
* thumb URI found.
*/
@Nullable private Uri getFirstImageUri(List<RbPageSummary> links) {
for (RbPageSummary link : links) {
if (link == null) {
continue;
}
String thumbnail = link.getThumbnailUrl();
if (thumbnail != null) {
return Uri.parse(thumbnail);
}
}
return null;
}
}

View file

@ -0,0 +1,80 @@
package org.wikipedia.feed.onthisday;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.util.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class OnThisDay {
@SuppressWarnings("unused") @Nullable private List<Event> selected;
@SuppressWarnings("unused") @Nullable private List<Event> events;
@SuppressWarnings("unused") @Nullable private List<Event> births;
@SuppressWarnings("unused") @Nullable private List<Event> deaths;
@SuppressWarnings("unused") @Nullable private List<Event> holidays;
@NonNull public List<Event> selectedEvents() {
return selected != null ? selected : Collections.emptyList();
}
@NonNull public List<Event> events() {
ArrayList<Event> allEvents = new ArrayList<>();
if (events != null) {
allEvents.addAll(events);
}
if (births != null) {
allEvents.addAll(births);
}
if (deaths != null) {
allEvents.addAll(deaths);
}
if (holidays != null) {
allEvents.addAll(holidays);
}
Collections.sort(allEvents, (e1, e2) -> Integer.compare(e2.year(), e1.year()));
return allEvents;
}
public static class Event {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String text;
@SuppressWarnings("unused") private int year;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private List<RbPageSummary> pages;
@NonNull
public CharSequence text() {
List<String> pageTitles = new ArrayList<>();
for (RbPageSummary page : pages) {
pageTitles.add((StringUtil.fromHtml(StringUtils.defaultString(page.getNormalizedTitle()))).toString());
}
return StringUtil.boldenSubstrings(text, pageTitles);
}
public int year() {
return year;
}
@Nullable
public List<RbPageSummary> pages() {
Iterator iterator = pages.iterator();
while ((iterator.hasNext())) {
if (iterator.next() == null) {
iterator.remove();
}
}
return pages;
}
}
public void setSelected(@Nullable List<Event> selected) {
this.selected = selected;
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.gallery;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
public class ArtistInfo extends TextInfo {
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("user_page") private String userPage;
@Nullable
public String getName() {
return name;
}
}

View file

@ -0,0 +1,102 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class ExtMetadata {
@SerializedName("DateTime") @Nullable private Values dateTime;
@SerializedName("ObjectName") @Nullable private Values objectName;
@SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension;
@SerializedName("Categories") @Nullable private Values categories;
@SerializedName("Assessments") @Nullable private Values assessments;
@SerializedName("GPSLatitude") @Nullable private Values gpsLatitude;
@SerializedName("GPSLongitude") @Nullable private Values gpsLongitude;
@SerializedName("ImageDescription") @Nullable private Values imageDescription;
@SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal;
@SerializedName("Artist") @Nullable private Values artist;
@SerializedName("Credit") @Nullable private Values credit;
@SerializedName("Permission") @Nullable private Values permission;
@SerializedName("AuthorCount") @Nullable private Values authorCount;
@SerializedName("LicenseShortName") @Nullable private Values licenseShortName;
@SerializedName("UsageTerms") @Nullable private Values usageTerms;
@SerializedName("LicenseUrl") @Nullable private Values licenseUrl;
@SerializedName("AttributionRequired") @Nullable private Values attributionRequired;
@SerializedName("Copyrighted") @Nullable private Values copyrighted;
@SerializedName("Restrictions") @Nullable private Values restrictions;
@SerializedName("License") @Nullable private Values license;
@NonNull public String licenseShortName() {
return StringUtils.defaultString(licenseShortName == null ? null : licenseShortName.value());
}
@NonNull public String licenseUrl() {
return StringUtils.defaultString(licenseUrl == null ? null : licenseUrl.value());
}
@NonNull public String license() {
return StringUtils.defaultString(license == null ? null : license.value());
}
@NonNull public String imageDescription() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.value());
}
@NonNull public String imageDescriptionSource() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.source());
}
@NonNull public String objectName() {
return StringUtils.defaultString(objectName == null ? null : objectName.value());
}
@NonNull public String usageTerms() {
return StringUtils.defaultString(usageTerms == null ? null : usageTerms.value());
}
@NonNull public String dateTimeOriginal() {
return StringUtils.defaultString(dateTimeOriginal == null ? null : dateTimeOriginal.value());
}
@NonNull public String dateTime() {
return StringUtils.defaultString(dateTime == null ? null : dateTime.value());
}
@NonNull public String artist() {
return StringUtils.defaultString(artist == null ? null : artist.value());
}
@NonNull public String getCategories() {
return StringUtils.defaultString(categories == null ? null : categories.value());
}
@NonNull public String getGpsLatitude() {
return StringUtils.defaultString(gpsLatitude == null ? null : gpsLatitude.value());
}
@NonNull public String getGpsLongitude() {
return StringUtils.defaultString(gpsLongitude == null ? null : gpsLongitude.value());
}
@NonNull public String credit() {
return StringUtils.defaultString(credit == null ? null : credit.value());
}
public class Values {
@Nullable private String value;
@Nullable private String source;
@Nullable private String hidden;
@Nullable public String value() {
return value;
}
@Nullable public String source() {
return source;
}
}
}

View file

@ -0,0 +1,35 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class Gallery {
@SuppressWarnings("unused,NullableProblems") @Nullable private String revision;
@SuppressWarnings("unused,NullableProblems") @Nullable private String tid;
@SuppressWarnings("unused") @Nullable private List<GalleryItem> items;
@Nullable
public List<GalleryItem> getAllItems() {
return items;
}
@NonNull
public List<GalleryItem> getItems(@NonNull String... types) {
List<GalleryItem> list = new ArrayList<>();
if (items != null) {
for (GalleryItem item : items) {
if (item.isShowInGallery()) {
for (String type : types) {
if (item.getType().contains(type)) {
list.add(item);
}
}
}
}
}
return list;
}
}

View file

@ -0,0 +1,194 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.Service;
import org.wikipedia.util.ImageUrlUtil;
import org.wikipedia.util.StringUtil;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class GalleryItem implements Serializable {
public static final int PREFERRED_GALLERY_IMAGE_SIZE = 1280;
@SerializedName("section_id") private int sectionId;
@SuppressWarnings("NullableProblems") @NonNull private String type;
@Nullable @SerializedName("audio_type") private String audioType;
@Nullable private TextInfo caption;
private boolean showInGallery;
@SuppressWarnings("NullableProblems") @NonNull private Titles titles;
@Nullable private ImageInfo thumbnail;
@Nullable private ImageInfo original;
@Nullable private List<VideoInfo> sources;
@Nullable @SerializedName("file_page") private String filePage;
@Nullable private ArtistInfo artist;
private double duration;
@SuppressWarnings("NullableProblems") @NonNull private ImageLicense license;
@Nullable private TextInfo description;
@Nullable @SerializedName("wb_entity_id") private String entityId;
@Nullable @SerializedName("structured") private StructuredData structuredData;
public GalleryItem() {
}
public GalleryItem(@NonNull String title) {
this.type = "*/*";
this.titles = new Titles(title, StringUtil.addUnderscores(title), title);
this.original = new ImageInfo();
this.thumbnail = new ImageInfo();
this.description = new TextInfo();
this.license = new ImageLicense();
}
@NonNull
public String getType() {
return StringUtils.defaultString(type);
}
@NonNull
public String getAudioType() {
return StringUtils.defaultString(audioType);
}
@Nullable
public TextInfo getCaption() {
return caption;
}
public boolean isShowInGallery() {
return showInGallery;
}
@NonNull
public Titles getTitles() {
return titles;
}
protected void setTitle(@NonNull String title) {
titles = new Titles(title, StringUtil.addUnderscores(title), title);
}
@NonNull
public ImageInfo getThumbnail() {
if (thumbnail == null) {
thumbnail = new ImageInfo();
}
return thumbnail;
}
@NonNull
public String getThumbnailUrl() {
return getThumbnail().getSource();
}
@NonNull
public String getPreferredSizedImageUrl() {
return ImageUrlUtil.getUrlForPreferredSize(getThumbnailUrl(), PREFERRED_GALLERY_IMAGE_SIZE);
}
@NonNull
public ImageInfo getOriginal() {
if (original == null) {
original = new ImageInfo();
}
return original;
}
@Nullable
public List<VideoInfo> getSources() {
return sources;
}
@Nullable
public VideoInfo getOriginalVideoSource() {
// The getSources has different levels of source,
// should have an option that allows user to chose which quality to play
return sources == null || sources.size() == 0
? null : sources.get(sources.size() - 1);
}
public double getDuration() {
return duration;
}
@NonNull
public String getFilePage() {
// return the base url of Wiki Commons for WikiSite() if the file_page is null.
return filePage == null ? Service.COMMONS_URL : StringUtils.defaultString(filePage);
}
public void setFilePage(@NonNull String filePage) {
this.filePage = filePage;
}
@Nullable
public ArtistInfo getArtist() {
return artist;
}
public void setArtist(@Nullable ArtistInfo artist) {
this.artist = artist;
}
@NonNull
public ImageLicense getLicense() {
return license;
}
public void setLicense(@NonNull ImageLicense license) {
this.license = license;
}
@NonNull
public TextInfo getDescription() {
if (description == null) {
description = new TextInfo();
}
return description;
}
@NonNull
public Map<String, String> getStructuredCaptions() {
return (structuredData != null && structuredData.captions != null) ? structuredData.captions : Collections.emptyMap();
}
public static class Titles implements Serializable {
@Nullable private String canonical;
@Nullable private String normalized;
@Nullable private String display;
Titles(@NonNull String display, @NonNull String canonical, @NonNull String normalized) {
this.display = display;
this.canonical = canonical;
this.normalized = normalized;
}
@NonNull
public String getCanonical() {
return StringUtils.defaultString(canonical);
}
@NonNull
public String getNormalized() {
return StringUtils.defaultString(normalized);
}
@NonNull
public String getDisplay() {
return StringUtils.defaultString(display);
}
}
public static class StructuredData implements Serializable {
@Nullable private HashMap<String, String> captions;
}
}

View file

@ -0,0 +1,76 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
/**
* Gson POJO for a standard image info object as returned by the API ImageInfo module
*/
@SuppressWarnings("unused")
public class ImageInfo implements Serializable {
private int size;
private int width;
private int height;
@Nullable private String source;
@SerializedName("thumburl") @Nullable private String thumbUrl;
@SerializedName("thumbwidth") private int thumbWidth;
@SerializedName("thumbheight") private int thumbHeight;
@SerializedName("url") @Nullable private String originalUrl;
@SerializedName("descriptionurl") @Nullable private String descriptionUrl;
@SerializedName("descriptionshorturl") @Nullable private String descriptionShortUrl;
@SerializedName("mime") @Nullable private String mimeType;
@SerializedName("extmetadata")@Nullable private ExtMetadata metadata;
@Nullable private String user;
@Nullable private String timestamp;
@NonNull
public String getSource() {
return StringUtils.defaultString(source);
}
public void setSource(@Nullable String source) {
this.source = source;
}
public int getSize() {
return size;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@NonNull public String getMimeType() {
return StringUtils.defaultString(mimeType, "*/*");
}
@NonNull public String getThumbUrl() {
return StringUtils.defaultString(thumbUrl);
}
@NonNull public String getOriginalUrl() {
return StringUtils.defaultString(originalUrl);
}
@NonNull public String getUser() {
return StringUtils.defaultString(user);
}
@NonNull public String getTimestamp() {
return StringUtils.defaultString(timestamp);
}
@Nullable public ExtMetadata getMetadata() {
return metadata;
}
}

View file

@ -0,0 +1,67 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Locale;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class ImageLicense implements Serializable {
private static final String CREATIVE_COMMONS_PREFIX = "cc";
private static final String PUBLIC_DOMAIN_PREFIX = "pd";
private static final String CC_BY_SA = "ccbysa";
@NonNull @SerializedName("type") private final String license;
@NonNull @SerializedName("code") private final String licenseShortName;
@NonNull @SerializedName("url") private final String licenseUrl;
public ImageLicense(@NonNull ExtMetadata metadata) {
this.license = metadata.license();
this.licenseShortName = metadata.licenseShortName();
this.licenseUrl = metadata.licenseUrl();
}
private ImageLicense(@NonNull String license, @NonNull String licenseShortName, @NonNull String licenseUrl) {
this.license = license;
this.licenseShortName = licenseShortName;
this.licenseUrl = licenseUrl;
}
public ImageLicense() {
this("", "", "");
}
@NonNull public String getLicenseName() {
return license;
}
@NonNull public String getLicenseShortName() {
return licenseShortName;
}
@NonNull public String getLicenseUrl() {
return licenseUrl;
}
public boolean isLicenseCC() {
return defaultString(license).toLowerCase(Locale.ENGLISH).startsWith(CREATIVE_COMMONS_PREFIX)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).startsWith(CREATIVE_COMMONS_PREFIX);
}
public boolean isLicensePD() {
return defaultString(license).toLowerCase(Locale.ENGLISH).startsWith(PUBLIC_DOMAIN_PREFIX)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).startsWith(PUBLIC_DOMAIN_PREFIX);
}
public boolean isLicenseCCBySa() {
return defaultString(license).toLowerCase(Locale.ENGLISH).replace("-", "").startsWith(CC_BY_SA)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).replace("-", "").startsWith(CC_BY_SA);
}
public boolean hasLicenseInfo() {
return !(license.isEmpty() && licenseShortName.isEmpty() && licenseUrl.isEmpty());
}
}

View file

@ -0,0 +1,29 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
public class TextInfo implements Serializable {
@SuppressWarnings("unused,NullableProblems") @Nullable private String html;
@SuppressWarnings("unused,NullableProblems") @Nullable private String text;
@SuppressWarnings("unused,NullableProblems") @Nullable private String lang;
@NonNull
public String getHtml() {
return StringUtils.defaultString(html);
}
@NonNull
public String getText() {
return StringUtils.defaultString(text);
}
@NonNull
public String getLang() {
return StringUtils.defaultString(lang);
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.gallery;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Gson POJO for a standard video info object as returned by the API VideoInfo module
*/
public class VideoInfo extends ImageInfo {
@SuppressWarnings("unused") @Nullable private List<String> codecs;
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("short_name") private String shortName;
}

View file

@ -0,0 +1,53 @@
package org.wikipedia.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class CookieManagerTypeAdapter extends TypeAdapter<SharedPreferenceCookieManager> {
@Override public void write(JsonWriter out, SharedPreferenceCookieManager cookies) throws IOException {
Map<String, List<Cookie>> map = cookies.getCookieJar();
out.beginObject();
for (String key : map.keySet()) {
out.name(key).beginArray();
for (Cookie cookie : map.get(key)) {
out.value(cookie.toString());
}
out.endArray();
}
out.endObject();
}
@Override public SharedPreferenceCookieManager read(JsonReader in) throws IOException {
Map<String, List<Cookie>> map = new HashMap<>();
in.beginObject();
while (in.hasNext()) {
String key = in.nextName();
List<Cookie> list = new ArrayList<>();
map.put(key, list);
in.beginArray();
HttpUrl url = HttpUrl.parse(WikiSite.DEFAULT_SCHEME + "://" + key);
while (in.hasNext()) {
String str = in.nextString();
if (url != null) {
list.add(Cookie.parse(url, str));
}
}
in.endArray();
}
in.endObject();
return new SharedPreferenceCookieManager(map);
}
}

View file

@ -0,0 +1,18 @@
package org.wikipedia.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
public final class GsonMarshaller {
public static String marshal(@Nullable Object object) {
return marshal(GsonUtil.getDefaultGson(), object);
}
public static String marshal(@NonNull Gson gson, @Nullable Object object) {
return gson.toJson(object);
}
private GsonMarshaller() { }
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public final class GsonUnmarshaller {
/** @return Unmarshalled object. */
public static <T> T unmarshal(Class<T> clazz, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), clazz, json);
}
/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(TypeToken<T> typeToken, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), typeToken, json);
}
/** @return Unmarshalled object. */
public static <T> T unmarshal(@NonNull Gson gson, Class<T> clazz, @Nullable String json) {
return gson.fromJson(json, clazz);
}
/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(@NonNull Gson gson, TypeToken<T> typeToken, @Nullable String json) {
// From the manual: "Fairly hideous... Unfortunately, no way to get around this in Java".
return gson.fromJson(json, typeToken.getType());
}
private GsonUnmarshaller() { }
}

View file

@ -0,0 +1,38 @@
package org.wikipedia.json;
import android.net.Uri;
import androidx.annotation.VisibleForTesting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.Namespace;
public final class GsonUtil {
private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss";
private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder()
.setDateFormat(DATE_FORMAT)
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe())
.registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe())
.registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe())
.registerTypeAdapter(SharedPreferenceCookieManager.class, new CookieManagerTypeAdapter().nullSafe())
.registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory())
.registerTypeAdapterFactory(new PostProcessingTypeAdapter());
private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create();
public static Gson getDefaultGson() {
return DEFAULT_GSON;
}
@VisibleForTesting
public static GsonBuilder getDefaultGsonBuilder() {
return DEFAULT_GSON_BUILDER;
}
private GsonUtil() { }
}

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