mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +01:00
With data-client added as library module (#3656)
* With data-client added as library module * Fix build
This commit is contained in:
parent
9ee04f3df4
commit
32ee0b4f9a
258 changed files with 34820 additions and 2 deletions
4
data-client/src/main/AndroidManifest.xml
Normal file
4
data-client/src/main/AndroidManifest.xml
Normal 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>
|
||||
38
data-client/src/main/java/org/wikipedia/AppAdapter.java
Normal file
38
data-client/src/main/java/org/wikipedia/AppAdapter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
19
data-client/src/main/java/org/wikipedia/captcha/Captcha.java
Normal file
19
data-client/src/main/java/org/wikipedia/captcha/Captcha.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
401
data-client/src/main/java/org/wikipedia/dataclient/Service.java
Normal file
401
data-client/src/main/java/org/wikipedia/dataclient/Service.java
Normal 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§ions=1-"
|
||||
+ "§ionprop=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"
|
||||
+ "§ions=0§ionprop=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¬format=model¬limit=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¬prop=list¬filter=!read¬limit=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§ionpreview=&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);
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
320
data-client/src/main/java/org/wikipedia/dataclient/WikiSite.java
Normal file
320
data-client/src/main/java/org/wikipedia/dataclient/WikiSite.java
Normal 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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
79
data-client/src/main/java/org/wikipedia/edit/Edit.java
Normal file
79
data-client/src/main/java/org/wikipedia/edit/Edit.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
72
data-client/src/main/java/org/wikipedia/edit/EditClient.java
Normal file
72
data-client/src/main/java/org/wikipedia/edit/EditClient.java
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
32
data-client/src/main/java/org/wikipedia/edit/EditResult.java
Normal file
32
data-client/src/main/java/org/wikipedia/edit/EditResult.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
102
data-client/src/main/java/org/wikipedia/gallery/ExtMetadata.java
Normal file
102
data-client/src/main/java/org/wikipedia/gallery/ExtMetadata.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
data-client/src/main/java/org/wikipedia/gallery/Gallery.java
Normal file
35
data-client/src/main/java/org/wikipedia/gallery/Gallery.java
Normal 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;
|
||||
}
|
||||
}
|
||||
194
data-client/src/main/java/org/wikipedia/gallery/GalleryItem.java
Normal file
194
data-client/src/main/java/org/wikipedia/gallery/GalleryItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
|
|
@ -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() { }
|
||||
}
|
||||
38
data-client/src/main/java/org/wikipedia/json/GsonUtil.java
Normal file
38
data-client/src/main/java/org/wikipedia/json/GsonUtil.java
Normal 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() { }
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.wikipedia.json;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import org.wikipedia.page.Namespace;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class NamespaceTypeAdapter extends TypeAdapter<Namespace> {
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Namespace namespace) throws IOException {
|
||||
out.value(namespace.code());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Namespace read(JsonReader in) throws IOException {
|
||||
if (in.peek() == JsonToken.STRING) {
|
||||
// Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of
|
||||
// the code number. This introduces a backwards-compatible check for the string value.
|
||||
// TODO: remove after April 2017, when all older namespaces have been deserialized.
|
||||
return Namespace.valueOf(in.nextString());
|
||||
}
|
||||
return Namespace.of(in.nextInt());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.wikipedia.json;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PostProcessingTypeAdapter implements TypeAdapterFactory {
|
||||
public interface PostProcessable {
|
||||
void postProcess();
|
||||
}
|
||||
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||
|
||||
return new TypeAdapter<T>() {
|
||||
public void write(JsonWriter out, T value) throws IOException {
|
||||
delegate.write(out, value);
|
||||
}
|
||||
|
||||
public T read(JsonReader in) throws IOException {
|
||||
T obj = delegate.read(in);
|
||||
if (obj instanceof PostProcessable) {
|
||||
((PostProcessable)obj).postProcess();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package org.wikipedia.json;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import org.wikipedia.json.annotations.Required;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
|
||||
* missing fields annotated with @Required.
|
||||
*
|
||||
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
|
||||
* contain null elements after deserialization!
|
||||
*
|
||||
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
|
||||
* annotation and another corresponding TypeAdapter(Factory).
|
||||
*/
|
||||
class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) {
|
||||
Class<?> rawType = typeToken.getRawType();
|
||||
Set<Field> requiredFields = collectRequiredFields(rawType);
|
||||
|
||||
if (requiredFields.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setFieldsAccessible(requiredFields, true);
|
||||
return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields);
|
||||
}
|
||||
|
||||
@NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) {
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
Set<Field> required = new ArraySet<>();
|
||||
for (Field field : fields) {
|
||||
if (field.isAnnotationPresent(Required.class)) {
|
||||
required.add(field);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(required);
|
||||
}
|
||||
|
||||
private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) {
|
||||
for (Field field : fields) {
|
||||
field.setAccessible(accessible);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Adapter<T> extends TypeAdapter<T> {
|
||||
@NonNull private final TypeAdapter<T> delegate;
|
||||
@NonNull private final Set<Field> requiredFields;
|
||||
|
||||
private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) {
|
||||
this.delegate = delegate;
|
||||
this.requiredFields = requiredFields;
|
||||
}
|
||||
|
||||
@Override public void write(JsonWriter out, T value) throws IOException {
|
||||
delegate.write(out, value);
|
||||
}
|
||||
|
||||
@Override @Nullable public T read(JsonReader in) throws IOException {
|
||||
T deserialized = delegate.read(in);
|
||||
return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null;
|
||||
}
|
||||
|
||||
private boolean allRequiredFieldsPresent(@NonNull T deserialized,
|
||||
@NonNull Set<Field> required) {
|
||||
for (Field field : required) {
|
||||
try {
|
||||
if (field.get(deserialized) == null) {
|
||||
return false;
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package org.wikipedia.json;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class UriTypeAdapter extends TypeAdapter<Uri> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Uri value) throws IOException {
|
||||
out.value(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri read(JsonReader in) throws IOException {
|
||||
String url = in.nextString();
|
||||
return Uri.parse(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package org.wikipedia.json;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class WikiSiteTypeAdapter extends TypeAdapter<WikiSite> {
|
||||
private static final String DOMAIN = "domain";
|
||||
private static final String LANGUAGE_CODE = "languageCode";
|
||||
|
||||
@Override public void write(JsonWriter out, WikiSite value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name(DOMAIN);
|
||||
out.value(value.url());
|
||||
|
||||
out.name(LANGUAGE_CODE);
|
||||
out.value(value.languageCode());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
@Override public WikiSite read(JsonReader in) throws IOException {
|
||||
// todo: legacy; remove in June 2018
|
||||
if (in.peek() == JsonToken.STRING) {
|
||||
return new WikiSite(Uri.parse(in.nextString()));
|
||||
}
|
||||
|
||||
String domain = null;
|
||||
String languageCode = null;
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String field = in.nextName();
|
||||
String val = in.nextString();
|
||||
switch (field) {
|
||||
case DOMAIN:
|
||||
domain = val;
|
||||
break;
|
||||
case LANGUAGE_CODE:
|
||||
languageCode = val;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
if (domain == null) {
|
||||
throw new JsonParseException("Missing domain");
|
||||
}
|
||||
|
||||
// todo: legacy; remove in June 2018
|
||||
if (languageCode == null) {
|
||||
return new WikiSite(domain);
|
||||
}
|
||||
return new WikiSite(domain, languageCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.wikipedia.json.annotations;
|
||||
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
|
||||
/**
|
||||
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
|
||||
* an instantiated object.
|
||||
*
|
||||
* E.g.: @NonNull @Required private String title;
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(FIELD)
|
||||
public @interface Required {
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package org.wikipedia.language;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class AcceptLanguageUtil {
|
||||
private static final float APP_LANGUAGE_QUALITY = .9f;
|
||||
private static final float SYSTEM_LANGUAGE_QUALITY = .8f;
|
||||
|
||||
/**
|
||||
* @return The value that should go in the Accept-Language header.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getAcceptLanguage(@NonNull String wikiLanguageCode,
|
||||
@NonNull String appLanguageCode,
|
||||
@NonNull String systemLanguageCode) {
|
||||
String acceptLanguage = wikiLanguageCode;
|
||||
acceptLanguage = appendToAcceptLanguage(acceptLanguage, appLanguageCode, APP_LANGUAGE_QUALITY);
|
||||
acceptLanguage = appendToAcceptLanguage(acceptLanguage, systemLanguageCode, SYSTEM_LANGUAGE_QUALITY);
|
||||
return acceptLanguage;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String appendToAcceptLanguage(@NonNull String acceptLanguage,
|
||||
@NonNull String languageCode, float quality) {
|
||||
// If accept-language already contains the language, just return accept-language.
|
||||
if (acceptLanguage.contains(languageCode)) {
|
||||
return acceptLanguage;
|
||||
}
|
||||
|
||||
// If accept-language is empty, don't append. Just return the language.
|
||||
if (acceptLanguage.isEmpty()) {
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
// Accept-language is nonempty, append the language.
|
||||
return String.format(Locale.ROOT, "%s,%s;q=%.1f", acceptLanguage, languageCode, quality);
|
||||
}
|
||||
|
||||
private AcceptLanguageUtil() { }
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package org.wikipedia.language;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.dataclient.R;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Immutable look up table for all app supported languages. All article languages may not be
|
||||
* present in this table as it is statically bundled with the app. */
|
||||
public class AppLanguageLookUpTable {
|
||||
public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans";
|
||||
public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant";
|
||||
public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn";
|
||||
public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk";
|
||||
public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo";
|
||||
public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg";
|
||||
public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw";
|
||||
public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue";
|
||||
public static final String CHINESE_LANGUAGE_CODE = "zh";
|
||||
public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no";
|
||||
public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb";
|
||||
public static final String TEST_LANGUAGE_CODE = "test";
|
||||
public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys.
|
||||
|
||||
@NonNull private final Resources resources;
|
||||
|
||||
// Language codes for all app supported languages in fixed order. The special code representing
|
||||
// the dynamic system language is null.
|
||||
@NonNull private SoftReference<List<String>> codesRef = new SoftReference<>(null);
|
||||
|
||||
// English names for all app supported languages in fixed order.
|
||||
@NonNull private SoftReference<List<String>> canonicalNamesRef = new SoftReference<>(null);
|
||||
|
||||
// Native names for all app supported languages in fixed order.
|
||||
@NonNull private SoftReference<List<String>> localizedNamesRef = new SoftReference<>(null);
|
||||
|
||||
public AppLanguageLookUpTable(@NonNull Context context) {
|
||||
resources = context.getResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Nonnull immutable list. The special code representing the dynamic system language is
|
||||
* null.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getCodes() {
|
||||
List<String> codes = codesRef.get();
|
||||
if (codes == null) {
|
||||
codes = getStringList(R.array.preference_language_keys);
|
||||
codesRef = new SoftReference<>(codes);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCanonicalName(@Nullable String code) {
|
||||
String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null);
|
||||
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
|
||||
if (code.equals(Locale.CHINESE.getLanguage())) {
|
||||
name = Locale.CHINESE.getDisplayName(Locale.ENGLISH);
|
||||
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
|
||||
name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getLocalizedName(@Nullable String code) {
|
||||
String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null);
|
||||
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
|
||||
if (code.equals(Locale.CHINESE.getLanguage())) {
|
||||
name = Locale.CHINESE.getDisplayName(Locale.CHINESE);
|
||||
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
|
||||
name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getCanonicalNames() {
|
||||
List<String> names = canonicalNamesRef.get();
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_canonical_names);
|
||||
canonicalNamesRef = new SoftReference<>(names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public List<String> getLocalizedNames() {
|
||||
List<String> names = localizedNamesRef.get();
|
||||
if (names == null) {
|
||||
names = getStringList(R.array.preference_language_local_names);
|
||||
localizedNamesRef = new SoftReference<>(names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public boolean isSupportedCode(@Nullable String code) {
|
||||
return getCodes().contains(code);
|
||||
}
|
||||
|
||||
private <T> T defaultIndex(List<T> list, int index, T defaultValue) {
|
||||
return inBounds(list, index) ? list.get(index) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches #codes for the specified language code and returns the index for use in
|
||||
* #canonicalNames and #localizedNames.
|
||||
*
|
||||
* @param code The language code to search for. The special code representing the dynamic system
|
||||
* language is null.
|
||||
* @return The index of the language code or -1 if the code is not supported.
|
||||
*/
|
||||
private int indexOfCode(@Nullable String code) {
|
||||
return getCodes().indexOf(code);
|
||||
}
|
||||
|
||||
/** @return Nonnull immutable list. */
|
||||
@NonNull
|
||||
private List<String> getStringList(int id) {
|
||||
return Arrays.asList(getStringArray(id));
|
||||
}
|
||||
|
||||
private boolean inBounds(List<?> list, int index) {
|
||||
return index >= 0 && index < list.size();
|
||||
}
|
||||
|
||||
public String[] getStringArray(@ArrayRes int id) {
|
||||
return resources.getStringArray(id);
|
||||
}
|
||||
}
|
||||
248
data-client/src/main/java/org/wikipedia/login/LoginClient.java
Normal file
248
data-client/src/main/java/org/wikipedia/login/LoginClient.java
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
package org.wikipedia.login;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.text.TextUtils;
|
||||
|
||||
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.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.dataclient.mwapi.ListUserResponse;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
import org.wikipedia.dataclient.mwapi.MwServiceError;
|
||||
import org.wikipedia.util.log.L;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* Responsible for making login related requests to the server.
|
||||
*/
|
||||
public class LoginClient {
|
||||
@Nullable private Call<MwQueryResponse> tokenCall;
|
||||
@Nullable private Call<LoginResponse> loginCall;
|
||||
|
||||
public interface LoginCallback {
|
||||
void success(@NonNull LoginResult result);
|
||||
void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token);
|
||||
void passwordResetPrompt(@Nullable String token);
|
||||
void error(@NonNull Throwable caught);
|
||||
}
|
||||
|
||||
public void request(@NonNull final WikiSite wiki, @NonNull final String userName,
|
||||
@NonNull final String password, @NonNull final LoginCallback cb) {
|
||||
cancel();
|
||||
|
||||
tokenCall = ServiceFactory.get(wiki).getLoginToken();
|
||||
tokenCall.enqueue(new Callback<MwQueryResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<MwQueryResponse> call,
|
||||
@NonNull Response<MwQueryResponse> response) {
|
||||
login(wiki, userName, password, null, null, response.body().query().loginToken(), cb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable caught) {
|
||||
if (call.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
cb.error(caught);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void login(@NonNull final WikiSite wiki, @NonNull final String userName, @NonNull final String password,
|
||||
@Nullable final String retypedPassword, @Nullable final String twoFactorCode,
|
||||
@Nullable final String loginToken, @NonNull final LoginCallback cb) {
|
||||
loginCall = TextUtils.isEmpty(twoFactorCode) && TextUtils.isEmpty(retypedPassword)
|
||||
? ServiceFactory.get(wiki).postLogIn(userName, password, loginToken, Service.WIKIPEDIA_URL)
|
||||
: ServiceFactory.get(wiki).postLogIn(userName, password, retypedPassword, twoFactorCode, loginToken, true);
|
||||
loginCall.enqueue(new Callback<LoginResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<LoginResponse> call, @NonNull Response<LoginResponse> response) {
|
||||
LoginResponse loginResponse = response.body();
|
||||
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
|
||||
if (loginResult != null) {
|
||||
if (loginResult.pass() && !TextUtils.isEmpty(loginResult.getUserName())) {
|
||||
// The server could do some transformations on user names, e.g. on some
|
||||
// wikis is uppercases the first letter.
|
||||
String actualUserName = loginResult.getUserName();
|
||||
getExtendedInfo(wiki, actualUserName, loginResult, cb);
|
||||
} else if ("UI".equals(loginResult.getStatus())) {
|
||||
if (loginResult instanceof LoginOAuthResult) {
|
||||
cb.twoFactorPrompt(new LoginFailedException(loginResult.getMessage()), loginToken);
|
||||
} else if (loginResult instanceof LoginResetPasswordResult) {
|
||||
cb.passwordResetPrompt(loginToken);
|
||||
} else {
|
||||
cb.error(new LoginFailedException(loginResult.getMessage()));
|
||||
}
|
||||
} else {
|
||||
cb.error(new LoginFailedException(loginResult.getMessage()));
|
||||
}
|
||||
} else {
|
||||
cb.error(new IOException("Login failed. Unexpected response."));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<LoginResponse> call, @NonNull Throwable t) {
|
||||
if (call.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
cb.error(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loginBlocking(@NonNull final WikiSite wiki, @NonNull final String userName,
|
||||
@NonNull final String password, @Nullable final String twoFactorCode) throws Throwable {
|
||||
Response<MwQueryResponse> tokenResponse = ServiceFactory.get(wiki).getLoginToken().execute();
|
||||
if (tokenResponse.body() == null || TextUtils.isEmpty(tokenResponse.body().query().loginToken())) {
|
||||
throw new IOException("Unexpected response when getting login token.");
|
||||
}
|
||||
String loginToken = tokenResponse.body().query().loginToken();
|
||||
|
||||
Call<LoginResponse> tempLoginCall = StringUtils.defaultIfEmpty(twoFactorCode, "").isEmpty()
|
||||
? ServiceFactory.get(wiki).postLogIn(userName, password, loginToken, Service.WIKIPEDIA_URL)
|
||||
: ServiceFactory.get(wiki).postLogIn(userName, password, null, twoFactorCode, loginToken, true);
|
||||
Response<LoginResponse> response = tempLoginCall.execute();
|
||||
LoginResponse loginResponse = response.body();
|
||||
if (loginResponse == null) {
|
||||
throw new IOException("Unexpected response when logging in.");
|
||||
}
|
||||
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
|
||||
if (loginResult == null) {
|
||||
throw new IOException("Unexpected response when logging in.");
|
||||
}
|
||||
if ("UI".equals(loginResult.getStatus())) {
|
||||
if (loginResult instanceof LoginOAuthResult) {
|
||||
|
||||
// TODO: Find a better way to boil up the warning about 2FA
|
||||
throw new LoginFailedException(loginResult.getMessage());
|
||||
|
||||
} else {
|
||||
throw new LoginFailedException(loginResult.getMessage());
|
||||
}
|
||||
} else if (!loginResult.pass() || TextUtils.isEmpty(loginResult.getUserName())) {
|
||||
throw new LoginFailedException(loginResult.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void getExtendedInfo(@NonNull final WikiSite wiki, @NonNull String userName,
|
||||
@NonNull final LoginResult loginResult, @NonNull final LoginCallback cb) {
|
||||
ServiceFactory.get(wiki).getUserInfo(userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> {
|
||||
ListUserResponse user = response.query().getUserResponse(userName);
|
||||
int id = response.query().userInfo().id();
|
||||
loginResult.setUserId(id);
|
||||
loginResult.setGroups(user.getGroups());
|
||||
cb.success(loginResult);
|
||||
L.v("Found user ID " + id + " for " + wiki.subdomain());
|
||||
}, caught -> {
|
||||
L.e("Login succeeded but getting group information failed. " + caught);
|
||||
cb.error(caught);
|
||||
});
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancelTokenRequest();
|
||||
cancelLogin();
|
||||
}
|
||||
|
||||
private void cancelTokenRequest() {
|
||||
if (tokenCall == null) {
|
||||
return;
|
||||
}
|
||||
tokenCall.cancel();
|
||||
tokenCall = null;
|
||||
}
|
||||
|
||||
private void cancelLogin() {
|
||||
if (loginCall == null) {
|
||||
return;
|
||||
}
|
||||
loginCall.cancel();
|
||||
loginCall = null;
|
||||
}
|
||||
|
||||
public static final class LoginResponse {
|
||||
@SuppressWarnings("unused") @SerializedName("error") @Nullable
|
||||
private MwServiceError error;
|
||||
|
||||
@SuppressWarnings("unused") @SerializedName("clientlogin") @Nullable
|
||||
private ClientLogin clientLogin;
|
||||
|
||||
@Nullable public MwServiceError getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Nullable LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
|
||||
return clientLogin != null ? clientLogin.toLoginResult(site, password) : null;
|
||||
}
|
||||
|
||||
private static class ClientLogin {
|
||||
@SuppressWarnings("unused,NullableProblems") @NonNull private String status;
|
||||
@SuppressWarnings("unused") @Nullable private List<Request> requests;
|
||||
@SuppressWarnings("unused") @Nullable private String message;
|
||||
@SuppressWarnings("unused") @SerializedName("username") @Nullable private String userName;
|
||||
|
||||
LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
|
||||
String userMessage = message;
|
||||
if ("UI".equals(status)) {
|
||||
if (requests != null) {
|
||||
for (Request req : requests) {
|
||||
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest".equals(req.id())) {
|
||||
return new LoginOAuthResult(site, status, userName, password, message);
|
||||
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest".equals(req.id())) {
|
||||
return new LoginResetPasswordResult(site, status, userName, password, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!"PASS".equals(status) && !"FAIL".equals(status)) {
|
||||
//TODO: String resource -- Looks like needed for others in this class too
|
||||
userMessage = "An unknown error occurred.";
|
||||
}
|
||||
return new LoginResult(site, status, userName, password, userMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Request {
|
||||
@SuppressWarnings("unused") @Nullable private String id;
|
||||
//@SuppressWarnings("unused") @Nullable private JsonObject metadata;
|
||||
@SuppressWarnings("unused") @Nullable private String required;
|
||||
@SuppressWarnings("unused") @Nullable private String provider;
|
||||
@SuppressWarnings("unused") @Nullable private String account;
|
||||
@SuppressWarnings("unused") @Nullable private Map<String, RequestField> fields;
|
||||
|
||||
@Nullable String id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestField {
|
||||
@SuppressWarnings("unused") @Nullable private String type;
|
||||
@SuppressWarnings("unused") @Nullable private String label;
|
||||
@SuppressWarnings("unused") @Nullable private String help;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoginFailedException extends Throwable {
|
||||
public LoginFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package org.wikipedia.login;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
||||
public class LoginOAuthResult extends LoginResult {
|
||||
|
||||
public LoginOAuthResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
||||
@Nullable String password, @Nullable String message) {
|
||||
super(site, status, userName, password, message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.wikipedia.login;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
||||
public class LoginResetPasswordResult extends LoginResult {
|
||||
public LoginResetPasswordResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
||||
@Nullable String password, @Nullable String message) {
|
||||
super(site, status, userName, password, message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package org.wikipedia.login;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class LoginResult {
|
||||
@NonNull private final WikiSite site;
|
||||
@NonNull private final String status;
|
||||
@Nullable private final String userName;
|
||||
@Nullable private final String password;
|
||||
@Nullable private final String message;
|
||||
|
||||
private int userId;
|
||||
@NonNull private Set<String> groups = Collections.emptySet();
|
||||
|
||||
public LoginResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
||||
@Nullable String password, @Nullable String message) {
|
||||
this.site = site;
|
||||
this.status = status;
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NonNull public WikiSite getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
@NonNull public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean pass() {
|
||||
return "PASS".equals(status);
|
||||
}
|
||||
|
||||
public boolean fail() {
|
||||
return "FAIL".equals(status);
|
||||
}
|
||||
|
||||
@Nullable public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
@Nullable public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Nullable public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setUserId(int id) {
|
||||
this.userId = id;
|
||||
}
|
||||
|
||||
public int getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setGroups(@NonNull Set<String> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
@NonNull public Set<String> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
19
data-client/src/main/java/org/wikipedia/model/BaseModel.kt
Normal file
19
data-client/src/main/java/org/wikipedia/model/BaseModel.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package org.wikipedia.model
|
||||
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder
|
||||
|
||||
abstract class BaseModel {
|
||||
override fun toString(): String {
|
||||
return ToStringBuilder.reflectionToString(this)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return HashCodeBuilder.reflectionHashCode(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return EqualsBuilder.reflectionEquals(this, other)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.wikipedia.model
|
||||
|
||||
interface CodeEnum<T> {
|
||||
fun enumeration(code: Int): T
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue