Data client simplification / removal (#5507)

* Removed unused code from the data client module

* Move remaining code out of the data-client and remove it
This commit is contained in:
Paul Hawke 2024-02-02 19:26:06 -06:00 committed by GitHub
parent 72a6fd2c90
commit 0c3085257d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
337 changed files with 964 additions and 31321 deletions

View file

@ -59,7 +59,7 @@ import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.language.AppLanguageLookUpTable;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
import timber.log.Timber;
@AcraCore(

View file

@ -3,8 +3,7 @@ package fr.free.nrw.commons
import android.os.Parcelable
import fr.free.nrw.commons.location.LatLng
import kotlinx.android.parcel.Parcelize
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.page.PageTitle
import fr.free.nrw.commons.wikidata.model.page.PageTitle
import java.util.*
@Parcelize

View file

@ -15,7 +15,6 @@ import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.dataclient.okhttp.HttpStatusException;
import timber.log.Timber;
public final class OkHttpConnectionFactory {
@ -110,4 +109,30 @@ public final class OkHttpConnectionFactory {
private OkHttpConnectionFactory() {
}
public static class HttpStatusException extends IOException {
private final int code;
private final String url;
public HttpStatusException(@NonNull Response rsp) {
this.code = rsp.code();
this.url = rsp.request().url().uri().toString();
try {
if (rsp.body() != null && rsp.body().contentType() != null
&& rsp.body().contentType().toString().contains("json")) {
}
} catch (Exception e) {
// Log?
}
}
public int code() {
return code;
}
@Override
public String getMessage() {
String str = "Code: " + code + ", URL: " + url;
return str;
}
}
}

View file

@ -20,8 +20,8 @@ import androidx.core.content.ContextCompat;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import java.util.Calendar;
import java.util.Date;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
import java.util.Locale;
import java.util.regex.Pattern;

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.actions
import org.wikipedia.dataclient.mwapi.MwResponse
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
/**
* Response of the Thanks API.

View file

@ -1,11 +1,11 @@
package fr.free.nrw.commons.actions
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.edit.Edit
import io.reactivex.Observable
import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.edit.Edit
import org.wikipedia.wikidata.Entities
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.http.*
/**

View file

@ -30,7 +30,7 @@ import fr.free.nrw.commons.auth.login.LoginResult;
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.Locale;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import fr.free.nrw.commons.auth.login.LoginCallback;
import javax.inject.Inject;

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.auth.csrf
import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.auth.SessionManager
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginFailedException

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.auth.csrf
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers

View file

@ -6,7 +6,7 @@ import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.auth.login
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import io.reactivex.Observable
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded

View file

@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.auth.login.LoginResult.Result
import org.wikipedia.dataclient.mwapi.MwServiceError
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
class LoginResponse {
@SerializedName("error")

View file

@ -12,7 +12,7 @@ import androidx.annotation.Nullable;
import fr.free.nrw.commons.campaigns.models.Campaign;
import fr.free.nrw.commons.theme.BaseActivity;
import org.wikipedia.util.DateUtil;
import fr.free.nrw.commons.utils.DateUtil;
import java.text.ParseException;
import java.util.Date;

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.category
import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -29,7 +29,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import java.util.ArrayList;
import java.util.List;
import org.wikipedia.page.PageTitle;
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
/**
* This activity displays details of a particular category

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.category;
import io.reactivex.Single;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;

View file

@ -52,7 +52,7 @@ import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.WikiSite;
import fr.free.nrw.commons.wikidata.model.WikiSite;
/**

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.delete;
import android.content.Context;
import org.wikipedia.util.DateUtil;
import fr.free.nrw.commons.utils.DateUtil;
import java.util.Date;
import java.util.Locale;

View file

@ -41,7 +41,6 @@ import java.util.Map;
import java.util.Objects;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.AppAdapter;
/**
* The Dependency Provider class for Commons Android.

View file

@ -45,8 +45,8 @@ import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import fr.free.nrw.commons.wikidata.GsonUtil;
import timber.log.Timber;
@Module

View file

@ -7,11 +7,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsInterface
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.upload.structure.depictions.get
import fr.free.nrw.commons.wikidata.WikidataProperties
import fr.free.nrw.commons.wikidata.model.DataValue
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.Statement_partial
import io.reactivex.Single
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -6,12 +6,12 @@ import fr.free.nrw.commons.upload.structure.depictions.get
import fr.free.nrw.commons.utils.CommonsDateUtil
import fr.free.nrw.commons.utils.MediaDataExtractorUtil
import fr.free.nrw.commons.wikidata.WikidataProperties
import fr.free.nrw.commons.wikidata.model.DataValue
import fr.free.nrw.commons.wikidata.model.Entities
import org.apache.commons.lang3.StringUtils
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.gallery.ExtMetadata
import org.wikipedia.gallery.ImageInfo
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import fr.free.nrw.commons.wikidata.model.gallery.ExtMetadata
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo
import java.text.ParseException
import java.util.*
import javax.inject.Inject

View file

@ -0,0 +1,141 @@
package fr.free.nrw.commons.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 fr.free.nrw.commons.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);
}
}

View file

@ -5,10 +5,10 @@ import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.ContinuationClient
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil
import fr.free.nrw.commons.wikidata.model.Entities
import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.wikidata.Entities
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -115,9 +115,9 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.util.DateUtil;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
import fr.free.nrw.commons.utils.DateUtil;
import timber.log.Timber;
public class MediaDetailFragment extends CommonsDaggerSupportFragment implements

View file

@ -2,10 +2,10 @@ package fr.free.nrw.commons.media;
import static fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX;
import fr.free.nrw.commons.wikidata.model.Entities;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.wikidata.Entities;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import retrofit2.http.GET;
import retrofit2.http.Query;

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.media;
import io.reactivex.Single;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.media;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.dataclient.mwapi.MwResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
public class MwParseResponse extends MwResponse {
@Nullable

View file

@ -4,10 +4,10 @@ import fr.free.nrw.commons.BetaConstants
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.ContinuationClient
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.wikidata.model.Entities
import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.wikidata.Entities
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.media;
import static fr.free.nrw.commons.media.MediaInterface.MEDIA_PARAMS;
import io.reactivex.Single;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import retrofit2.http.GET;
import retrofit2.http.Query;

View file

@ -17,8 +17,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import timber.log.Timber;
/**

View file

@ -1,10 +1,9 @@
package fr.free.nrw.commons.mwapi;
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResult;
import org.wikipedia.dataclient.mwapi.UserInfo;
import org.wikipedia.util.DateUtil;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResult;
import fr.free.nrw.commons.wikidata.mwapi.UserInfo;
import fr.free.nrw.commons.utils.DateUtil;
import java.util.Collections;
import java.util.Date;
@ -44,16 +43,4 @@ public class UserClient {
}
}).single(false);
}
public Observable<MwQueryLogEvent> logEvents(String user) {
try {
return userInterface.getUserLogEvents(user, Collections.emptyMap())
.map(MwQueryResponse::query)
.map(MwQueryResult::logevents)
.flatMap(Observable::fromIterable);
} catch (Throwable throwable) {
return Observable.empty();
}
}
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.mwapi;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import java.util.Map;

View file

@ -9,8 +9,8 @@ import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import org.wikipedia.model.EnumCode;
import org.wikipedia.model.EnumCodeMap;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
import fr.free.nrw.commons.R;

View file

@ -7,8 +7,8 @@ import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import org.wikipedia.model.EnumCode;
import org.wikipedia.model.EnumCodeMap;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
public enum NavTabLoggedOut implements EnumCode {

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.databinding.ItemNotificationBinding
import org.wikipedia.util.StringUtil
import fr.free.nrw.commons.utils.StringUtil
fun notificationDelegate(onNotificationClicked: (Notification) -> Unit) =

View file

@ -6,12 +6,12 @@ import fr.free.nrw.commons.notification.models.NotificationType
import io.reactivex.Observable
import io.reactivex.Single
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.util.DateUtil
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import fr.free.nrw.commons.utils.DateUtil
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import org.wikipedia.notifications.Notification as WikimediaNotification
import fr.free.nrw.commons.wikidata.model.notifications.Notification as WikimediaNotification
@Singleton
class NotificationClient @Inject constructor(

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.notification
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import io.reactivex.Observable
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET

View file

@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import java.util.ArrayList;
import java.util.concurrent.Callable;

View file

@ -13,7 +13,7 @@ import java.util.Collections;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import timber.log.Timber;
@Singleton

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.review;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import io.reactivex.Observable;
import retrofit2.http.GET;

View file

@ -6,7 +6,7 @@ import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatTextView;
import org.wikipedia.util.StringUtil;
import fr.free.nrw.commons.utils.StringUtil;
/**
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any

View file

@ -11,7 +11,7 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.RowItemLanguagesSpinnerBinding
import fr.free.nrw.commons.utils.LangCodeUtils
import org.apache.commons.lang3.StringUtils
import org.wikipedia.language.AppLanguageLookUpTable
import fr.free.nrw.commons.language.AppLanguageLookUpTable
import java.util.*
/**
@ -39,7 +39,8 @@ class LanguagesAdapter constructor(
private var languageNamesList: List<String>
private var languageCodesList: List<String>
var language: AppLanguageLookUpTable = AppLanguageLookUpTable(context)
var language: AppLanguageLookUpTable =
AppLanguageLookUpTable(context)
init {
languageNamesList = language.localizedNames
languageCodesList = language.codes

View file

@ -27,7 +27,7 @@ import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.mwapi.MwException;
import fr.free.nrw.commons.wikidata.mwapi.MwException;
import timber.log.Timber;
@Singleton

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.upload
import android.os.Parcel
import android.os.Parcelable
import org.wikipedia.gallery.ImageInfo
private const val RESULT_SUCCESS = "Success"

View file

@ -4,8 +4,8 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX;
import androidx.annotation.NonNull;
import io.reactivex.Observable;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.upload.depicts;
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
import fr.free.nrw.commons.wikidata.model.Entities;
import io.reactivex.Single;
import org.wikipedia.wikidata.Entities;
import retrofit2.http.GET;
import retrofit2.http.Query;

View file

@ -8,10 +8,10 @@ import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties
import fr.free.nrw.commons.wikidata.WikidataProperties.*
import fr.free.nrw.commons.wikidata.model.DataValue
import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.Statement_partial
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

View file

@ -0,0 +1,53 @@
package fr.free.nrw.commons.utils;
import static android.text.format.DateFormat.getBestDateTimePattern;
import androidx.annotation.NonNull;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
public final class DateUtil {
private static Map<String, SimpleDateFormat> DATE_FORMATS = new HashMap<>();
// TODO: Switch to DateTimeFormatter when minSdk = 26.
public static synchronized String iso8601DateFormat(Date date) {
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date);
}
public static synchronized Date iso8601DateParse(String date) throws ParseException {
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date);
}
public static String getMonthOnlyDateString(@NonNull Date date) {
return getDateStringWithSkeletonPattern(date, "MMMM d");
}
public static String getExtraShortDateString(@NonNull Date date) {
return getDateStringWithSkeletonPattern(date, "MMM d");
}
public static synchronized String getDateStringWithSkeletonPattern(@NonNull Date date, @NonNull String pattern) {
return getCachedDateFormat(getBestDateTimePattern(Locale.getDefault(), pattern), Locale.getDefault(), false).format(date);
}
private static SimpleDateFormat getCachedDateFormat(String pattern, Locale locale, boolean utc) {
if (!DATE_FORMATS.containsKey(pattern)) {
SimpleDateFormat df = new SimpleDateFormat(pattern, locale);
if (utc) {
df.setTimeZone(TimeZone.getTimeZone("UTC"));
}
DATE_FORMATS.put(pattern, df);
}
return DATE_FORMATS.get(pattern);
}
private DateUtil() {
}
}

View file

@ -0,0 +1,38 @@
package fr.free.nrw.commons.utils;
import android.os.Build;
import android.text.Html;
import android.text.Spanned;
import android.text.SpannedString;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class StringUtil {
/**
* @param source String that may contain HTML tags.
* @return returned Spanned string that may contain spans parsed from the HTML source.
*/
@NonNull public static Spanned fromHtml(@Nullable String source) {
if (source == null) {
return new SpannedString("");
}
if (!source.contains("<") && !source.contains("&")) {
// If the string doesn't contain any hints of HTML entities, then skip the expensive
// processing that fromHtml() performs.
return new SpannedString(source);
}
source = source.replaceAll("&#8206;", "\u200E")
.replaceAll("&#8207;", "\u200F")
.replaceAll("&amp;", "&");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
return Html.fromHtml(source);
}
}
private StringUtil() {
}
}

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.wikidata
import okhttp3.OkHttpClient
import org.wikipedia.json.GsonUtil
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.wikidata;
import android.net.Uri;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory;
import fr.free.nrw.commons.wikidata.model.DataValue;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter;
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
import fr.free.nrw.commons.wikidata.json.UriTypeAdapter;
import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter;
import fr.free.nrw.commons.wikidata.model.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)
.registerTypeAdapterFactory(DataValue.getPolymorphicTypeAdapter())
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe())
.registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe())
.registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe())
.registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory())
.registerTypeAdapterFactory(new PostProcessingTypeAdapter());
private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create();
public static Gson getDefaultGson() {
return DEFAULT_GSON;
}
private GsonUtil() { }
}

View file

@ -10,7 +10,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
import timber.log.Timber;
/**

View file

@ -9,7 +9,7 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import org.wikipedia.wikidata.Statement_partial;
import fr.free.nrw.commons.wikidata.model.Statement_partial;
@Singleton
public class WikidataClient {

View file

@ -15,6 +15,12 @@ import fr.free.nrw.commons.upload.WikidataItem;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.model.DataValue;
import fr.free.nrw.commons.wikidata.model.DataValue.ValueString;
import fr.free.nrw.commons.wikidata.model.EditClaim;
import fr.free.nrw.commons.wikidata.model.Snak_partial;
import fr.free.nrw.commons.wikidata.model.Statement_partial;
import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
@ -27,13 +33,7 @@ import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.wikidata.DataValue;
import org.wikipedia.wikidata.DataValue.ValueString;
import org.wikipedia.wikidata.EditClaim;
import org.wikipedia.wikidata.Snak_partial;
import org.wikipedia.wikidata.Statement_partial;
import org.wikipedia.wikidata.WikiBaseMonolingualTextValue;
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
import timber.log.Timber;
/**

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.wikidata;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.model.WbCreateClaimResponse;
import io.reactivex.Observable;

View file

@ -8,7 +8,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.wikipedia.dataclient.WikiSite
import fr.free.nrw.commons.wikidata.model.WikiSite
private const val COOKIE_STORE = "cookie_store"

View file

@ -0,0 +1,29 @@
package fr.free.nrw.commons.wikidata.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 fr.free.nrw.commons.wikidata.model.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());
}
}

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.wikidata.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;
}
};
}
}

View file

@ -0,0 +1,94 @@
package fr.free.nrw.commons.wikidata.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 fr.free.nrw.commons.wikidata.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).
*/
public 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;
}
}
}

View file

@ -0,0 +1,280 @@
package fr.free.nrw.commons.wikidata.json;
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.util.Log;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {@code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*
* <h3>Serialization and deserialization</h3>
* In order to serialize and deserialize a polymorphic object,
* you must specify the base type explicitly.
* <pre> {@code
* Diamond diamond = new Diamond();
* String json = gson.toJson(diamond, Shape.class);
* }</pre>
* And then:
* <pre> {@code
* Shape shape = gson.fromJson(json, Shape.class);
* }</pre>
*/
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private final boolean maintainType;
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
this.maintainType = maintainType;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
* {@code maintainType} flag decide if the type will be stored in pojo or not.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, maintainType);
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, false);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(baseType, "type", false);
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement;
if (maintainType) {
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
Log.e("RuntimeTypeAdapter", "cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype? " +jsonElement);
return null;
}
return delegate.fromJsonTree(jsonElement);
}
@Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (maintainType) {
Streams.write(jsonObject, out);
return;
}
JsonObject clone = new JsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}.nullSafe();
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.wikidata.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);
}
}

View file

@ -0,0 +1,63 @@
package fr.free.nrw.commons.wikidata.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 fr.free.nrw.commons.wikidata.model.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);
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.wikidata.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 {
}

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.wikidata.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)
}
}

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.wikidata.model
import fr.free.nrw.commons.wikidata.json.RuntimeTypeAdapterFactory
sealed class DataValue(val type: String) {
companion object {
@JvmStatic
val polymorphicTypeAdapter =
RuntimeTypeAdapterFactory.of(DataValue::class.java, DataValue::type.name)
.registerSubtype(EntityId::class.java, EntityId.TYPE)
.registerSubtype(ValueString::class.java, ValueString.TYPE)
.registerSubtype(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
.registerSubtype(Time_partial::class.java, Time_partial.TYPE)
.registerSubtype(Quantity_partial::class.java, Quantity_partial.TYPE)
.registerSubtype(MonoLingualText::class.java, MonoLingualText.TYPE)
}
// "value": {
// "entity-type": "item",
// "id": "Q30",
// "numeric-id": 30
// },
// "type": "wikibase-entityid"
// }
data class EntityId(val value: WikiBaseEntityValue) : DataValue(TYPE) {
companion object {
const val TYPE = "wikibase-entityid"
}
}
// {
// "value": "SomePicture.jpg",
// "type": "string"
// }
data class ValueString(val value: String) : DataValue(TYPE) {
companion object {
const val TYPE = "string"
}
}
// "value": {
// "latitude": 37.7733,
// "longitude": -122.412255,
// "altitude": null,
// "precision": 1.0e-6,
// "globe": "http://www.wikidata.org/entity/Q2"
// },
// "type": "globecoordinate"
// }
class GlobeCoordinate_partial() : DataValue(TYPE) {
companion object {
const val TYPE = "globecoordinate"
}
}
// "value": {
// "time": "+2019-12-03T00:00:00Z",
// "timezone": 0,
// "before": 0,
// "after": 0,
// "precision": 11,
// "calendarmodel": "http://www.wikidata.org/entity/Q1985727"
// },
// "type": "time"
// }
class Time_partial() : DataValue(TYPE) {
companion object {
const val TYPE = "time"
}
}
// {
// "value": {
// "amount": "+587",
// "unit": "http://www.wikidata.org/entity/Q828224"
// }
// }
class Quantity_partial() : DataValue(TYPE) {
companion object {
const val TYPE = "quantity"
}
}
// {
// "value": {
// "text": "활",
// "language": "ko"
// }
// }
class MonoLingualText(val value: WikiBaseMonolingualTextValue) : DataValue(TYPE) {
companion object {
const val TYPE = "monolingualtext"
}
}
}

View file

@ -0,0 +1,33 @@
package fr.free.nrw.commons.wikidata.model
data class EditClaim(val claims: List<Statement_partial>) {
companion object {
@JvmStatic
fun from(entityIds: List<String>, propertyName: String): EditClaim {
val list = mutableListOf<Statement_partial>()
entityIds.forEach {
list.add(
Statement_partial(
Snak_partial(
"value",
propertyName,
DataValue.EntityId(
WikiBaseEntityValue(
"item",
it,
it.removePrefix("Q").toLong()
)
)
),
"statement",
"preferred"
)
)
}
return EditClaim(list)
}
}
}

View file

@ -0,0 +1,106 @@
package fr.free.nrw.commons.wikidata.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
public class Entities extends MwResponse {
@Nullable private Map<String, Entity> entities;
private int success;
@NotNull
public Map<String, Entity> entities() {
return entities != null ? entities : Collections.emptyMap();
}
public int getSuccess() {
return success;
}
@Nullable public Entity getFirst() {
if (entities == null) {
return null;
}
return entities.values().iterator().next();
}
@Override
public void postProcess() {
if (getFirst() != null && getFirst().isMissing()) {
throw new RuntimeException("The requested entity was not found.");
}
}
public static class Entity {
@Nullable private String type;
@Nullable private String id;
@Nullable private Map<String, Label> labels;
@Nullable private Map<String, Label> descriptions;
@Nullable private Map<String, SiteLink> sitelinks;
@Nullable @SerializedName(value = "statements", alternate = "claims") private Map<String, List<Statement_partial>> statements;
@Nullable private String missing;
@NonNull public String id() {
return StringUtils.defaultString(id);
}
@NonNull public Map<String, Label> labels() {
return labels != null ? labels : Collections.emptyMap();
}
@NonNull public Map<String, Label> descriptions() {
return descriptions != null ? descriptions : Collections.emptyMap();
}
@NonNull public Map<String, SiteLink> sitelinks() {
return sitelinks != null ? sitelinks : Collections.emptyMap();
}
@Nullable
public Map<String, List<Statement_partial>> getStatements() {
return statements;
}
boolean isMissing() {
return "-1".equals(id) && missing != null;
}
}
public static class Label {
@Nullable private String language;
@Nullable private String value;
public Label(@Nullable final String language, @Nullable final String value) {
this.language = language;
this.value = value;
}
@NonNull public String language() {
return StringUtils.defaultString(language);
}
@NonNull public String value() {
return StringUtils.defaultString(value);
}
}
public static class SiteLink {
@Nullable private String site;
@Nullable private String title;
@NonNull public String getSite() {
return StringUtils.defaultString(site);
}
@NonNull public String getTitle() {
return StringUtils.defaultString(title);
}
}
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.wikidata.model
interface EnumCode {
fun code(): Int
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.wikidata.model
import android.util.SparseArray
class EnumCodeMap<T>(enumeration: Class<T>) where T : Enum<T>, T : EnumCode {
private val map: SparseArray<T>
init {
map = codeToEnumMap(enumeration)
}
operator fun get(code: Int): T {
return map.get(code) ?: throw IllegalArgumentException("code=$code")
}
private fun codeToEnumMap(enumeration: Class<T>): SparseArray<T> {
val ret = SparseArray<T>()
for (value in enumeration.enumConstants) {
ret.put(value.code(), value)
}
return ret
}
fun size(): Int {
return map.size()
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.wikidata.model
import com.google.gson.annotations.SerializedName
/*"mainsnak": {
"snaktype": "value",
"property": "P17",
"datatype": "wikibase-item",
"datavalue": {
"value": {
"entity-type": "item",
"id": "Q30",
"numeric-id": 30
},
"type": "wikibase-entityid"
}
}*/
data class Snak_partial(
@SerializedName("snaktype") val snakType: String,
val property: String,
@SerializedName("datavalue") val dataValue: DataValue
)

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.wikidata.model
import com.google.gson.annotations.SerializedName
/*{
"id": "q60$5083E43C-228B-4E3E-B82A-4CB20A22A3FB",
"mainsnak": {},
"type": "statement",
"rank": "normal",
"qualifiers": {
"P580": [],
"P5436": []
}
"references": [
{
"hash": "d103e3541cc531fa54adcaffebde6bef28d87d32",
"snaks": []
}
]
}*/
data class Statement_partial(
@SerializedName("mainsnak") val mainSnak: Snak_partial,
val type: String,
val rank: String,
val id: String? = null,
val qualifiers: Map<String, List<Snak_partial>> = mapOf(),
@SerializedName("qualifiers-order") val qualifiersOrder: List<String> = listOf()
)

View file

@ -0,0 +1,14 @@
package fr.free.nrw.commons.wikidata.model
import com.google.gson.annotations.SerializedName
/*"value": {
"entity-type": "item",
"id": "Q30",
"numeric-id": 30
}*/
data class WikiBaseEntityValue(
@SerializedName("entity-type") val entityType: String,
val id: String,
val numericId: Long
)

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.wikidata.model
import com.google.gson.annotations.SerializedName
/*"value": {
"type": "monolingualtext",
"value": {
"text": "some value",
"language": "en"
}
}*/
data class WikiBaseMonolingualTextValue(val text: String, val language: String)

View file

@ -0,0 +1,292 @@
package fr.free.nrw.commons.wikidata.model;
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.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
/**
* 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 {
private static String WIKIPEDIA_URL = "https://wikipedia.org/";
public static final String DEFAULT_SCHEME = "https";
private static String DEFAULT_BASE_URL = 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 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 = getLanguageVariantFromUri(tempUri);
if (!TextUtils.isEmpty(langVariant)) {
languageCode = langVariant;
} else {
languageCode = authorityToLanguageCode(authority);
}
this.uri = new Uri.Builder()
.scheme(tempUri.getScheme())
.encodedAuthority(authority)
.build();
}
/** Get language variant code from a Uri, e.g. "zh-*", otherwise returns empty string. */
@NonNull
private String getLanguageVariantFromUri(@NonNull Uri uri) {
if (TextUtils.isEmpty(uri.getPath())) {
return "";
}
String[] parts = StringUtils.split(StringUtils.defaultString(uri.getPath()), '/');
return parts.length > 1 && !parts[0].equals("wiki") ? parts[0] : "";
}
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());
}
/**
* Get wiki's mobile URL
* Eg. https://en.m.wikipedia.org
* @return
*/
public String mobileUrl() {
return String.format("%1$s://%2$s", scheme(), mobileAuthority());
}
@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;
}
// Auto-generated
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WikiSite wiki = (WikiSite) o;
if (!uri.equals(wiki.uri)) {
return false;
}
return languageCode.equals(wiki.languageCode);
}
// Auto-generated
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + languageCode.hashCode();
return result;
}
// Auto-generated
@Override
public String toString() {
return "WikiSite{"
+ "uri=" + uri
+ ", languageCode='" + languageCode + '\''
+ '}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(uri, 0);
dest.writeString(languageCode);
}
protected WikiSite(@NonNull Parcel in) {
this.uri = in.readParcelable(Uri.class.getClassLoader());
this.languageCode = in.readString();
}
@NonNull
private static String languageCodeToSubdomain(@NonNull String languageCode) {
switch (languageCode) {
case AppLanguageLookUpTable.SIMPLIFIED_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.TRADITIONAL_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_CN_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_HK_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_MO_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_SG_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_TW_LANGUAGE_CODE:
return AppLanguageLookUpTable.CHINESE_LANGUAGE_CODE;
case AppLanguageLookUpTable.NORWEGIAN_BOKMAL_LANGUAGE_CODE:
return AppLanguageLookUpTable.NORWEGIAN_LEGACY_LANGUAGE_CODE; // T114042
default:
return languageCode;
}
}
@NonNull private static String authorityToLanguageCode(@NonNull String authority) {
String[] parts = authority.split("\\.");
final int minLengthForSubdomain = 3;
if (parts.length < minLengthForSubdomain
|| parts.length == minLengthForSubdomain && parts[0].equals("m")) {
// ""
// wikipedia.org
// m.wikipedia.org
return "";
}
return parts[0];
}
@NonNull private static Uri ensureScheme(@NonNull Uri uri) {
if (TextUtils.isEmpty(uri.getScheme())) {
return uri.buildUpon().scheme(DEFAULT_SCHEME).build();
}
return uri;
}
/** @param authority Host and optional port. */
@NonNull private String authorityToMobile(@NonNull String authority) {
if (authority.startsWith("m.") || authority.contains(".m.")) {
return authority;
}
return authority.replaceFirst("^" + subdomain() + "\\.?", "$0m.");
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.wikidata.model.edit;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse;
public class Edit extends MwPostResponse {
@Nullable private Result edit;
@Nullable public Result edit() {
return edit;
}
public class Result {
@Nullable private String result;
@Nullable private String code;
@Nullable private String info;
@Nullable private String warning;
public boolean editSucceeded() {
return "Success".equals(result);
}
@Nullable public String code() {
return code;
}
@Nullable public String info() {
return info;
}
@Nullable public String warning() {
return warning;
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,121 @@
package fr.free.nrw.commons.wikidata.model.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
*/
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;
/**
* Query width, default width parameter of the API query in pixels.
*/
final private static int QUERY_WIDTH = 640;
/**
* Threshold height, the minimum height of the image in pixels.
*/
final private static int THRESHOLD_HEIGHT = 220;
@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;
}
/**
* Get the thumbnail width.
* @return
*/
public int getThumbWidth() { return thumbWidth; }
/**
* Get the thumbnail height.
* @return
*/
public int getThumbHeight() { return thumbHeight; }
@NonNull public String getMimeType() {
return StringUtils.defaultString(mimeType, "*/*");
}
@NonNull public String getThumbUrl() {
updateThumbUrl();
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;
}
/**
* Updates the ThumbUrl if image dimensions are not sufficient.
* Specifically, in panoramic images the height retrieved is less than required due to large width to height ratio,
* so we update the thumb url keeping a minimum height threshold.
*/
private void updateThumbUrl() {
// If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT
if(getThumbHeight() < THRESHOLD_HEIGHT){
// If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. )
if(getThumbWidth() == QUERY_WIDTH){
// Calculate new width depending on the aspect ratio.
final int finalWidth = (int)(THRESHOLD_HEIGHT * getThumbWidth() * 1.0 / getThumbHeight());
thumbHeight = THRESHOLD_HEIGHT;
thumbWidth = finalWidth;
final String toReplace = "/" + QUERY_WIDTH + "px";
final int position = thumbUrl.lastIndexOf(toReplace);
thumbUrl = (new StringBuilder(thumbUrl)).replace(position, position + toReplace.length(), "/" + thumbWidth + "px").toString();
}
}
}
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.wikidata.model.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 {
@Nullable private List<String> codecs;
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("short_name") private String shortName;
}

View file

@ -0,0 +1,190 @@
package fr.free.nrw.commons.wikidata.model.notifications;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.utils.DateUtil;
import fr.free.nrw.commons.wikidata.GsonUtil;
import java.text.ParseException;
import java.util.Date;
import timber.log.Timber;
public class Notification {
@Nullable private String wiki;
private long id;
@Nullable private String type;
@Nullable private String category;
@Nullable private Title title;
@Nullable private Timestamp timestamp;
@SerializedName("*") @Nullable private Contents contents;
@NonNull public String wiki() {
return StringUtils.defaultString(wiki);
}
public long id() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public long key() {
return id + wiki().hashCode();
}
@NonNull public String type() {
return StringUtils.defaultString(type);
}
@Nullable public Title title() {
return title;
}
@Nullable public Contents getContents() {
return contents;
}
public void setContents(@Nullable final Contents contents) {
this.contents = contents;
}
@NonNull public Date getTimestamp() {
return timestamp != null ? timestamp.date() : new Date();
}
public void setTimestamp(@Nullable final Timestamp timestamp) {
this.timestamp = timestamp;
}
@NonNull String getUtcIso8601() {
return StringUtils.defaultString(timestamp != null ? timestamp.utciso8601 : null);
}
public boolean isFromWikidata() {
return wiki().equals("wikidatawiki");
}
@Override public String toString() {
return Long.toString(id);
}
public static class Title {
@Nullable private String full;
@Nullable private String text;
@NonNull public String text() {
return StringUtils.defaultString(text);
}
@NonNull public String full() {
return StringUtils.defaultString(full);
}
}
public static class Timestamp {
@Nullable private String utciso8601;
public void setUtciso8601(@Nullable final String utciso8601) {
this.utciso8601 = utciso8601;
}
public Date date() {
try {
return DateUtil.iso8601DateParse(utciso8601);
} catch (ParseException e) {
Timber.e(e);
return new Date();
}
}
}
public static class Link {
@Nullable private String url;
@Nullable private String label;
@Nullable private String tooltip;
@Nullable private String description;
@Nullable private String icon;
@NonNull public String getUrl() {
return StringUtils.defaultString(url);
}
public void setUrl(@Nullable final String url) {
this.url = url;
}
@NonNull public String getTooltip() {
return StringUtils.defaultString(tooltip);
}
@NonNull public String getLabel() {
return StringUtils.defaultString(label);
}
@NonNull public String getIcon() {
return StringUtils.defaultString(icon);
}
}
public static class Links {
@Nullable private JsonElement primary;
private Link primaryLink;
public void setPrimary(@Nullable final JsonElement primary) {
this.primary = primary;
}
@Nullable public Link getPrimary() {
if (primary == null) {
return null;
}
if (primaryLink == null && primary instanceof JsonObject) {
primaryLink = GsonUtil.getDefaultGson().fromJson(primary, Link.class);
}
return primaryLink;
}
}
public static class Contents {
@Nullable private String header;
@Nullable private String compactHeader;
@Nullable private String body;
@Nullable private String icon;
@Nullable private Links links;
@NonNull public String getHeader() {
return StringUtils.defaultString(header);
}
@NonNull public String getCompactHeader() {
return StringUtils.defaultString(compactHeader);
}
public void setCompactHeader(@Nullable final String compactHeader) {
this.compactHeader = compactHeader;
}
@NonNull public String getBody() {
return StringUtils.defaultString(body);
}
@Nullable public Links getLinks() {
return links;
}
public void setLinks(@Nullable final Links links) {
this.links = links;
}
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
public final class GeoMarshaller {
@Nullable
public static String marshal(@Nullable Location object) {
if (object == null) {
return null;
}
JSONObject jsonObj = new JSONObject();
try {
jsonObj.put(GeoUnmarshaller.LATITUDE, object.getLatitude());
jsonObj.put(GeoUnmarshaller.LONGITUDE, object.getLongitude());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return jsonObj.toString();
}
private GeoMarshaller() { }
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
public final class GeoUnmarshaller {
static final String LATITUDE = "latitude";
static final String LONGITUDE = "longitude";
@Nullable
public static Location unmarshal(@Nullable String json) {
if (json == null) {
return null;
}
JSONObject jsonObj;
try {
jsonObj = new JSONObject(json);
} catch (JSONException e) {
return null;
}
return unmarshal(jsonObj);
}
@Nullable
public static Location unmarshal(@NonNull JSONObject jsonObj) {
Location ret = new Location((String) null);
ret.setLatitude(jsonObj.optDouble(LATITUDE));
ret.setLongitude(jsonObj.optDouble(LONGITUDE));
return ret;
}
private GeoUnmarshaller() { }
}

View file

@ -0,0 +1,174 @@
package fr.free.nrw.commons.wikidata.model.page;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
/** An enumeration describing the different possible namespace codes. Do not attempt to use this
* class to preserve URL path information such as Talk: or User: or localization.
* @see <a href='https://en.wikipedia.org/wiki/Wikipedia:Namespace'>Wikipedia:Namespace</a>
* @see <a href='https://www.mediawiki.org/wiki/Extension_default_namespaces'>Extension default namespaces</a>
* @see <a href='https://github.com/wikimedia/wikipedia-ios/blob/master/Wikipedia/Code/NSNumber+MWKTitleNamespace.h'>NSNumber+MWKTitleNamespace.h (iOS implementation)</a>
* @see <a href='https://www.mediawiki.org/wiki/Manual:Namespace#Built-in_namespaces'>Manual:Namespace</a>
* @see <a href='https://en.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=namespaces|namespacealiases'>Namespaces reported by API</a>
*/
public enum Namespace implements EnumCode {
MEDIA(-2),
SPECIAL(-1) {
@Override
public boolean talk() {
return false;
}
},
MAIN(0), // Main or Article
TALK(1),
USER(2),
USER_TALK(3),
PROJECT(4), // WP alias
PROJECT_TALK(5), // WT alias
FILE(6), // Image alias
FILE_TALK(7), // Image talk alias
MEDIAWIKI(8),
MEDIAWIKI_TALK(9),
TEMPLATE(10),
TEMPLATE_TALK(11),
HELP(12),
HELP_TALK(13),
CATEGORY(14),
CATEGORY_TALK(15),
THREAD(90),
THREAD_TALK(91),
SUMMARY(92),
SUMMARY_TALK(93),
PORTAL(100),
PORTAL_TALK(101),
PROPERTY(102),
PROPERTY_TALK(103),
TYPE(104),
TYPE_TALK(105),
FORM(106),
FORM_TALK(107),
BOOK(108),
BOOK_TALK(109),
FORUM(110),
FORUM_TALK(111),
DRAFT(118),
DRAFT_TALK(119),
USER_GROUP(160),
ACL(162),
FILTER(170),
FILTER_TALK(171),
USER_WIKI(200),
USER_WIKI_TALK(201),
USER_PROFILE(202),
USER_PROFILE_TALK(203),
ANNOTATION(248),
ANNOTATION_TALK(249),
PAGE(250),
PAGE_TALK(251),
INDEX(252),
INDEX_TALK(253),
MATH(262),
MATH_TALK(263),
WIDGET(274),
WIDGET_TALK(275),
JS_APPLET(280),
JS_APPLET_TALK(281),
POLL(300),
POLL_TALK(301),
COURSE(350),
COURSE_TALK(351),
MAPS_LAYER(420),
MAPS_LAYER_TALK(421),
QUIZ(430),
QUIZ_TALK(431),
EDUCATION_PROGRAM(446),
EDUCATION_PROGRAM_TALK(447),
BOILERPLATE(450),
BOILERPLATE_TALK(451),
CAMPAIGN(460),
CAMPAIGN_TALK(461),
SCHEMA(470),
SCHEMA_TALK(471),
JSON_CONFIG(482),
JSON_CONFIG_TALK(483),
GRAPH(484),
GRAPH_TALK(485),
JSON_DATA(486),
JSON_DATA_TALK(487),
NOVA_RESOURCE(488),
NOVA_RESOURCE_TALK(489),
GW_TOOLSET(490),
GW_TOOLSET_TALK(491),
BLOG(500),
BLOG_TALK(501),
USER_BOX(600),
USER_BOX_TALK(601),
LINK(700),
LINK_TALK(701),
TIMED_TEXT(710),
TIMED_TEXT_TALK(711),
GIT_ACCESS_ROOT(730),
GIT_ACCESS_ROOT_TALK(731),
INTERPRETATION(800),
INTERPRETATION_TALK(801),
MUSTACHE(806),
MUSTACHE_TALK(807),
JADE(810),
JADE_TALK(811),
R(814),
R_TALK(815),
MODULE(828),
MODULE_TALK(829),
SECURE_POLL(830),
SECURE_POLL_TALK(831),
COMMENT_STREAM(844),
COMMENT_STREAM_TALK(845),
CN_BANNER(866),
CN_BANNER_TALK(867),
GRAM(1024),
GRAM_TALK(1025),
TRANSLATIONS(1198),
TRANSLATIONS_TALK(1199),
GADGET(2300),
GADGET_TALK(2301),
GADGET_DEFINITION(2302),
GADGET_DEFINITION_TALK(2303),
TOPIC(2600);
private static final int TALK_MASK = 0x1;
private static final EnumCodeMap<Namespace> MAP = new EnumCodeMap<>(Namespace.class);
private final int code;
@NonNull
public static Namespace of(int code) {
return MAP.get(code);
}
@Override
public int code() {
return code;
}
public boolean special() {
return this == SPECIAL;
}
public boolean main() {
return this == MAIN;
}
public boolean file() {
return this == FILE;
}
public boolean talk() {
return (code & TALK_MASK) == TALK_MASK;
}
Namespace(int code) {
this.code = code;
}
}

View file

@ -0,0 +1,158 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Date;
import static org.apache.commons.lang3.StringUtils.defaultString;
/**
* Immutable class that contains metadata associated with a PageTitle.
*/
public class PageProperties implements Parcelable {
private final int pageId;
@NonNull private final Namespace namespace;
private final long revisionId;
private final Date lastModified;
private final String displayTitleText;
private final String editProtectionStatus;
private final int languageCount;
private final boolean isMainPage;
private final boolean isDisambiguationPage;
/** Nullable URL with no scheme. For example, foo.bar.com/ instead of http://foo.bar.com/. */
@Nullable private final String leadImageUrl;
@Nullable private final String leadImageName;
@Nullable private final String titlePronunciationUrl;
@Nullable private final Location geo;
@Nullable private final String wikiBaseItem;
@Nullable private final String descriptionSource;
/**
* True if the user who first requested this page can edit this page
* FIXME: This is not a true page property, since it depends on current user.
*/
private final boolean canEdit;
public int getPageId() {
return pageId;
}
public boolean isMainPage() {
return isMainPage;
}
public boolean isDisambiguationPage() {
return isDisambiguationPage;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(pageId);
parcel.writeInt(namespace.code());
parcel.writeLong(revisionId);
parcel.writeLong(lastModified.getTime());
parcel.writeString(displayTitleText);
parcel.writeString(titlePronunciationUrl);
parcel.writeString(GeoMarshaller.marshal(geo));
parcel.writeString(editProtectionStatus);
parcel.writeInt(languageCount);
parcel.writeInt(canEdit ? 1 : 0);
parcel.writeInt(isMainPage ? 1 : 0);
parcel.writeInt(isDisambiguationPage ? 1 : 0);
parcel.writeString(leadImageUrl);
parcel.writeString(leadImageName);
parcel.writeString(wikiBaseItem);
parcel.writeString(descriptionSource);
}
private PageProperties(Parcel in) {
pageId = in.readInt();
namespace = Namespace.of(in.readInt());
revisionId = in.readLong();
lastModified = new Date(in.readLong());
displayTitleText = in.readString();
titlePronunciationUrl = in.readString();
geo = GeoUnmarshaller.unmarshal(in.readString());
editProtectionStatus = in.readString();
languageCount = in.readInt();
canEdit = in.readInt() == 1;
isMainPage = in.readInt() == 1;
isDisambiguationPage = in.readInt() == 1;
leadImageUrl = in.readString();
leadImageName = in.readString();
wikiBaseItem = in.readString();
descriptionSource = in.readString();
}
public static final Parcelable.Creator<PageProperties> CREATOR
= new Parcelable.Creator<PageProperties>() {
@Override
public PageProperties createFromParcel(Parcel in) {
return new PageProperties(in);
}
@Override
public PageProperties[] newArray(int size) {
return new PageProperties[size];
}
};
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PageProperties that = (PageProperties) o;
return pageId == that.pageId
&& namespace == that.namespace
&& revisionId == that.revisionId
&& lastModified.equals(that.lastModified)
&& displayTitleText.equals(that.displayTitleText)
&& TextUtils.equals(titlePronunciationUrl, that.titlePronunciationUrl)
&& (geo == that.geo || geo != null && geo.equals(that.geo))
&& languageCount == that.languageCount
&& canEdit == that.canEdit
&& isMainPage == that.isMainPage
&& isDisambiguationPage == that.isDisambiguationPage
&& TextUtils.equals(editProtectionStatus, that.editProtectionStatus)
&& TextUtils.equals(leadImageUrl, that.leadImageUrl)
&& TextUtils.equals(leadImageName, that.leadImageName)
&& TextUtils.equals(wikiBaseItem, that.wikiBaseItem);
}
@Override
public int hashCode() {
int result = lastModified.hashCode();
result = 31 * result + displayTitleText.hashCode();
result = 31 * result + (titlePronunciationUrl != null ? titlePronunciationUrl.hashCode() : 0);
result = 31 * result + (geo != null ? geo.hashCode() : 0);
result = 31 * result + (editProtectionStatus != null ? editProtectionStatus.hashCode() : 0);
result = 31 * result + languageCount;
result = 31 * result + (isMainPage ? 1 : 0);
result = 31 * result + (isDisambiguationPage ? 1 : 0);
result = 31 * result + (leadImageUrl != null ? leadImageUrl.hashCode() : 0);
result = 31 * result + (leadImageName != null ? leadImageName.hashCode() : 0);
result = 31 * result + (wikiBaseItem != null ? wikiBaseItem.hashCode() : 0);
result = 31 * result + (canEdit ? 1 : 0);
result = 31 * result + pageId;
result = 31 * result + namespace.code();
result = 31 * result + (int) revisionId;
return result;
}
}

View file

@ -0,0 +1,339 @@
package fr.free.nrw.commons.wikidata.model.page;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Locale;
import timber.log.Timber;
/**
* Represents certain vital information about a page, including the title, namespace,
* and fragment (section anchor target). It can also contain a thumbnail URL for the
* page, and a short description retrieved from Wikidata.
*
* WARNING: This class is not immutable! Specifically, the thumbnail URL and the Wikidata
* description can be altered after construction. Therefore do NOT rely on all the fields
* of a PageTitle to remain constant for the lifetime of the object.
*/
public class PageTitle implements Parcelable {
public static final Parcelable.Creator<PageTitle> CREATOR
= new Parcelable.Creator<PageTitle>() {
@Override
public PageTitle createFromParcel(Parcel in) {
return new PageTitle(in);
}
@Override
public PageTitle[] newArray(int size) {
return new PageTitle[size];
}
};
/**
* The localised namespace of the page as a string, or null if the page is in mainspace.
*
* This field contains the prefix of the page's title, as opposed to the namespace ID used by
* MediaWiki. Therefore, mainspace pages always have a null namespace, as they have no prefix,
* and the namespace of a page will depend on the language of the wiki the user is currently
* looking at.
*
* Examples:
* * [[Manchester]] on enwiki will have a namespace of null
* * [[Deutschland]] on dewiki will have a namespace of null
* * [[User:Deskana]] on enwiki will have a namespace of "User"
* * [[Utilisateur:Deskana]] on frwiki will have a namespace of "Utilisateur", even if you got
* to the page by going to [[User:Deskana]] and having MediaWiki automatically redirect you.
*/
// TODO: remove. This legacy code is the localized namespace name (File, Special, Talk, etc) but
// isn't consistent across titles. e.g., articles with colons, such as RTÉ News: Six One,
// are broken.
@Nullable private final String namespace;
@NonNull private final String text;
@Nullable private final String fragment;
@Nullable private String thumbUrl;
@SerializedName("site") @NonNull private final WikiSite wiki;
@Nullable private String description;
@Nullable private final PageProperties properties;
// TODO: remove after the restbase endpoint supports ZH variants.
@Nullable private String convertedText;
/**
* Creates a new PageTitle object.
* Use this if you want to pass in a fragment portion separately from the title.
*
* @param prefixedText title of the page with optional namespace prefix
* @param fragment optional fragment portion
* @param wiki the wiki site the page belongs to
* @return a new PageTitle object matching the given input parameters
*/
public static PageTitle withSeparateFragment(@NonNull String prefixedText,
@Nullable String fragment, @NonNull WikiSite wiki) {
if (TextUtils.isEmpty(fragment)) {
return new PageTitle(prefixedText, wiki, null, (PageProperties) null);
} else {
// TODO: this class needs some refactoring to allow passing in a fragment
// without having to do string manipulations.
return new PageTitle(prefixedText + "#" + fragment, wiki, null, (PageProperties) null);
}
}
public PageTitle(@Nullable final String namespace, @NonNull String text, @Nullable String fragment, @Nullable String thumbUrl, @NonNull WikiSite wiki) {
this.namespace = namespace;
this.text = text;
this.fragment = fragment;
this.wiki = wiki;
this.thumbUrl = thumbUrl;
properties = null;
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description, @Nullable PageProperties properties) {
this(text, wiki, thumbUrl, properties);
this.description = description;
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description) {
this(text, wiki, thumbUrl);
this.description = description;
}
public PageTitle(@Nullable String namespace, @NonNull String text, @NonNull WikiSite wiki) {
this(namespace, text, null, null, wiki);
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl) {
this(text, wiki, thumbUrl, (PageProperties) null);
}
public PageTitle(@Nullable String text, @NonNull WikiSite wiki) {
this(text, wiki, null);
}
private PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl,
@Nullable PageProperties properties) {
if (text == null) {
text = "";
}
// FIXME: Does not handle mainspace articles with a colon in the title well at all
String[] fragParts = text.split("#", -1);
text = fragParts[0];
if (fragParts.length > 1) {
this.fragment = decodeURL(fragParts[1]).replace(" ", "_");
} else {
this.fragment = null;
}
String[] parts = text.split(":", -1);
if (parts.length > 1) {
String namespaceOrLanguage = parts[0];
if (Arrays.asList(Locale.getISOLanguages()).contains(namespaceOrLanguage)) {
this.namespace = null;
this.wiki = new WikiSite(wiki.authority(), namespaceOrLanguage);
} else {
this.wiki = wiki;
this.namespace = namespaceOrLanguage;
}
this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.length));
} else {
this.wiki = wiki;
this.namespace = null;
this.text = parts[0];
}
this.thumbUrl = thumbUrl;
this.properties = properties;
}
/**
* Decodes a URL-encoded string into its UTF-8 equivalent. If the string cannot be decoded, the
* original string is returned.
* @param url The URL-encoded string that you wish to decode.
* @return The decoded string, or the input string if the decoding failed.
*/
@NonNull private String decodeURL(@NonNull String url) {
try {
return URLDecoder.decode(url, "UTF-8");
} catch (IllegalArgumentException e) {
// Swallow IllegalArgumentException (can happen with malformed encoding), and just
// return the original string.
Timber.d("URL decoding failed. String was: %s", url);
return url;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@NonNull public WikiSite getWikiSite() {
return wiki;
}
@NonNull public String getText() {
return text.replace(" ", "_");
}
@Nullable public String getFragment() {
return fragment;
}
@Nullable public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(@Nullable String thumbUrl) {
this.thumbUrl = thumbUrl;
}
@Nullable public String getDescription() {
return description;
}
public void setDescription(@Nullable String description) {
this.description = description;
}
@NonNull
public String getConvertedText() {
return convertedText == null ? getPrefixedText() : convertedText;
}
public void setConvertedText(@Nullable String convertedText) {
this.convertedText = convertedText;
}
@NonNull public String getDisplayText() {
return getPrefixedText().replace("_", " ");
}
@NonNull public String getDisplayTextWithoutNamespace() {
return text.replace("_", " ");
}
public boolean hasProperties() {
return properties != null;
}
@Nullable public PageProperties getProperties() {
return properties;
}
public boolean isMainPage() {
return properties != null && properties.isMainPage();
}
public boolean isDisambiguationPage() {
return properties != null && properties.isDisambiguationPage();
}
public String getCanonicalUri() {
return getUriForDomain(getWikiSite().authority());
}
public String getMobileUri() {
return getUriForDomain(getWikiSite().mobileAuthority());
}
public String getUriForAction(String action) {
try {
return String.format(
"%1$s://%2$s/w/index.php?title=%3$s&action=%4$s",
getWikiSite().scheme(),
getWikiSite().authority(),
URLEncoder.encode(getPrefixedText(), "utf-8"),
action
);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public String getPrefixedText() {
// TODO: find a better way to check if the namespace is a ISO Alpha2 Code (two digits country code)
return namespace == null ? getText() : addUnderscores(namespace) + ":" + getText();
}
private String addUnderscores(@NonNull String text) {
return text.replace(" ", "_");
}
@Override public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(namespace);
parcel.writeString(text);
parcel.writeString(fragment);
parcel.writeParcelable(wiki, flags);
parcel.writeParcelable(properties, flags);
parcel.writeString(thumbUrl);
parcel.writeString(description);
parcel.writeString(convertedText);
}
@Override public boolean equals(Object o) {
if (!(o instanceof PageTitle)) {
return false;
}
PageTitle other = (PageTitle)o;
// Not using namespace directly since that can be null
return normalizedEquals(other.getPrefixedText(), getPrefixedText()) && other.wiki.equals(wiki);
}
// Compare two strings based on their normalized form, using the Unicode Normalization Form C.
// This should be used when comparing or verifying strings that will be exchanged between
// different platforms (iOS, desktop, etc) that may encode strings using inconsistent
// composition, especially for accents, diacritics, etc.
private boolean normalizedEquals(@Nullable String str1, @Nullable String str2) {
if (str1 == null || str2 == null) {
return (str1 == null && str2 == null);
}
return Normalizer.normalize(str1, Normalizer.Form.NFC)
.equals(Normalizer.normalize(str2, Normalizer.Form.NFC));
}
@Override public int hashCode() {
int result = getPrefixedText().hashCode();
result = 31 * result + wiki.hashCode();
return result;
}
@Override public String toString() {
return getPrefixedText();
}
@Override public int describeContents() {
return 0;
}
private String getUriForDomain(String domain) {
try {
return String.format(
"%1$s://%2$s/wiki/%3$s%4$s",
getWikiSite().scheme(),
domain,
URLEncoder.encode(getPrefixedText(), "utf-8"),
(this.fragment != null && this.fragment.length() > 0) ? ("#" + this.fragment) : ""
);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private PageTitle(Parcel in) {
namespace = in.readString();
text = in.readString();
fragment = in.readString();
wiki = in.readParcelable(WikiSite.class.getClassLoader());
properties = in.readParcelable(PageProperties.class.getClassLoader());
thumbUrl = in.readString();
description = in.readString();
convertedText = in.readString();
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
public class ImageDetails {
private String name;
private String title;
@NonNull public String getName() {
return name;
}
@NonNull public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.wikidata.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;
public class ListUserResponse {
@SerializedName("name") @Nullable private String name;
private long userid;
@Nullable private List<String> groups;
@Nullable public String name() {
return name;
}
@NonNull public Set<String> getGroups() {
return groups != null ? new ArraySet<>(groups) : Collections.emptySet();
}
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
import java.util.List;
public class MwException extends RuntimeException {
@Nullable private final MwServiceError error;
@Nullable private final List<MwServiceError> errors;
public MwException(@Nullable MwServiceError error,
@Nullable final List<MwServiceError> errors) {
this.error = error;
this.errors = errors;
}
public String getErrorCode() {
if(error!=null) {
return error.getCode();
}
return errors != null ? errors.get(0).getCode() : null;
}
@Nullable public MwServiceError getError() {
return error;
}
@Nullable
public String getTitle() {
if (error != null) {
return error.getTitle();
}
return errors != null ? errors.get(0).getTitle() : null;
}
@Override
@Nullable
public String getMessage() {
if (error != null) {
return error.getDetails();
}
return errors != null ? errors.get(0).getDetails() : null;
}
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
public class MwPostResponse extends MwResponse {
private int success;
public boolean success(@Nullable String result) {
return "success".equals(result);
}
public int getSuccessVal() {
return success;
}
}

View file

@ -0,0 +1,229 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo;
import fr.free.nrw.commons.wikidata.model.BaseModel;
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 {
private int pageid;
private int index;
@NonNull private String title;
@NonNull private CategoryInfo categoryinfo;
@Nullable private List<Revision> revisions;
@SerializedName("fileusage") @Nullable private List<FileUsage> fileUsages;
@SerializedName("globalusage") @Nullable private List<GlobalUsage> globalUsages;
@Nullable private List<Coordinates> coordinates;
@Nullable private List<Category> categories;
@Nullable private Thumbnail thumbnail;
@Nullable private String description;
@SerializedName("imageinfo") @Nullable private List<ImageInfo> imageInfo;
@Nullable private String redirectFrom;
@Nullable private String convertedFrom;
@Nullable private String convertedTo;
@NonNull public String title() {
return title;
}
@NonNull public CategoryInfo categoryInfo() {
return categoryinfo;
}
public int index() {
return index;
}
@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 int pageId() {
return pageid;
}
@Nullable public String thumbUrl() {
return thumbnail != null ? thumbnail.source() : null;
}
@Nullable public String description() {
return description;
}
@Nullable public ImageInfo imageInfo() {
return imageInfo != null ? imageInfo.get(0) : null;
}
public void redirectFrom(@Nullable String from) {
redirectFrom = from;
}
public void convertedFrom(@Nullable String from) {
convertedFrom = from;
}
public void convertedTo(@Nullable String to) {
convertedTo = to;
}
public void appendTitleFragment(@Nullable String fragment) {
title += "#" + fragment;
}
public boolean checkWhetherFileIsUsedInWikis() {
if (globalUsages != null && globalUsages.size() > 0) {
return true;
}
if (fileUsages == null || fileUsages.size() == 0) {
return false;
}
final int totalCount = fileUsages.size();
/* Ignore usage under https://commons.wikimedia.org/wiki/User:Didym/Mobile_upload/
which has been a gallery of all of our uploads since 2014 */
for (final FileUsage fileUsage : fileUsages) {
if ( ! fileUsage.title().contains("User:Didym/Mobile upload")) {
return true;
}
}
return false;
}
public static class Revision {
@SerializedName("revid") private long revisionId;
private String user;
@SerializedName("contentformat") @NonNull private String contentFormat;
@SerializedName("contentmodel") @NonNull private String contentModel;
@SerializedName("timestamp") @NonNull private String timeStamp;
@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 Coordinates {
@Nullable private Double lat;
@Nullable private Double lon;
@Nullable public Double lat() {
return lat;
}
@Nullable public Double lon() {
return lon;
}
}
public static class CategoryInfo {
private boolean hidden;
private int size;
private int pages;
private int files;
private int subcats;
public boolean isHidden() {
return hidden;
}
}
static class Thumbnail {
private String source;
private int width;
private int height;
String source() {
return source;
}
}
public static class GlobalUsage {
@SerializedName("title") private String title;
@SerializedName("wiki")private String wiki;
@SerializedName("url") private String url;
public String getTitle() {
return title;
}
public String getWiki() {
return wiki;
}
public String getUrl() {
return url;
}
}
public static class FileUsage {
@SerializedName("pageid") private int pageid;
@SerializedName("ns") private int ns;
@SerializedName("title") private String title;
public int pageId() {
return pageid;
}
public int ns() {
return ns;
}
public String title() {
return title;
}
}
public static class Category {
private int ns;
@SuppressWarnings("unused,NullableProblems") @Nullable private String title;
private boolean hidden;
public int ns() {
return ns;
}
@NonNull public String title() {
return StringUtils.defaultString(title);
}
public boolean hidden() {
return hidden;
}
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class MwQueryResponse extends MwResponse {
@SerializedName("continue") @Nullable private Map<String, String> continuation;
@SerializedName("query") @Nullable private MwQueryResult query;
@Nullable public Map<String, String> continuation() {
return continuation;
}
@Nullable public MwQueryResult query() {
return query;
}
public boolean success() {
return query != null;
}
}

View file

@ -0,0 +1,187 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo;
import fr.free.nrw.commons.wikidata.model.BaseModel;
import fr.free.nrw.commons.wikidata.model.notifications.Notification;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MwQueryResult extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@SerializedName("pages") @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;
@Nullable private NotificationList notifications;
@SerializedName("allimages") @Nullable private List<ImageDetails> allImages;
@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;
}
@Nullable public UserInfo userInfo() {
return userInfo;
}
@Nullable public String csrfToken() {
return tokens != null ? tokens.csrf() : null;
}
@Nullable public String loginToken() {
return tokens != null ? tokens.login() : null;
}
@Nullable public NotificationList notifications() {
return notifications;
}
@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;
}
@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());
}
}
}
}
private static class Redirect {
private int index;
@Nullable private String from;
@Nullable private String to;
@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 {
@Nullable private String from;
@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 NotificationList {
@Nullable
private List<Notification> list;
@Nullable
public List<Notification> list() {
return list;
}
}
}

View file

@ -0,0 +1,23 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter;
import fr.free.nrw.commons.wikidata.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), errors);
}
}
}

View file

@ -0,0 +1,29 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.wikidata.model.BaseModel;
/**
* Gson POJO for a MediaWiki API error.
*/
public class MwServiceError extends BaseModel {
@Nullable private String code;
@Nullable private String text;
@NonNull public String getTitle() {
return StringUtils.defaultString(code);
}
@NonNull public String getDetails() {
return StringUtils.defaultString(text);
}
@Nullable
public String getCode() {
return code;
}
}

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.wikidata.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Map;
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 String blockexpiry() {
if (blockexpiry != null)
return blockexpiry;
else return "";
}
}

View file

@ -0,0 +1,894 @@
<?xml version='1.0' encoding='utf-8'?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string-array name="preference_language_keys">
<item>en</item>
<item>es</item>
<item>de</item>
<item>ja</item>
<item>fr</item>
<item>ru</item>
<item>pt</item>
<item>it</item>
<item>zh-hans</item>
<item>zh-hant</item>
<item>ar</item>
<item>ko</item>
<item>id</item>
<item>pl</item>
<item>nl</item>
<item>fa</item>
<item>hi</item>
<item>th</item>
<item>vi</item>
<item>sv</item>
<item>uk</item>
<item>cs</item>
<item>simple</item>
<item>hu</item>
<item>ro</item>
<item>fi</item>
<item>el</item>
<item>he</item>
<item>nb</item>
<item>da</item>
<item>sr</item>
<item>hr</item>
<item>ms</item>
<item>bg</item>
<item>ca</item>
<item>tr</item>
<item>sk</item>
<item>sh</item>
<item>bn</item>
<item>tl</item>
<item>mr</item>
<item>ta</item>
<item>kk</item>
<item>lt</item>
<item>az</item>
<item>bs</item>
<item>sl</item>
<item>sq</item>
<item>arz</item>
<item>zh-yue</item>
<item>ka</item>
<item>te</item>
<item>et</item>
<item>lv</item>
<item>ml</item>
<item>hy</item>
<item>uz</item>
<item>kn</item>
<item>af</item>
<item>nn</item>
<item>mk</item>
<item>gl</item>
<item>sw</item>
<item>eu</item>
<item>ur</item>
<item>ky</item>
<item>gu</item>
<item>bh</item>
<item>sco</item>
<item>ast</item>
<item>is</item>
<item>mn</item>
<item>be</item>
<item>an</item>
<item>km</item>
<item>si</item>
<item>ceb</item>
<item>jv</item>
<item>eo</item>
<item>als</item>
<item>ig</item>
<item>su</item>
<item>be-x-old</item>
<item>la</item>
<item>my</item>
<item>cy</item>
<item>ne</item>
<item>bar</item>
<item>azb</item>
<item>mzn</item>
<item>as</item>
<item>am</item>
<item>so</item>
<item>pa</item>
<item>map-bms</item>
<item>scn</item>
<item>tg</item>
<item>ckb</item>
<item>ga</item>
<item>lb</item>
<item>war</item>
<item>zh-min-nan</item>
<item>nds</item>
<item>fy</item>
<item>vec</item>
<item>pnb</item>
<item>zh-classical</item>
<item>lmo</item>
<item>tt</item>
<item>io</item>
<item>ia</item>
<item>br</item>
<item>hif</item>
<item>mg</item>
<item>wuu</item>
<item>gan</item>
<item>ang</item>
<item>or</item>
<item>oc</item>
<item>yi</item>
<item>ps</item>
<item>tk</item>
<item>ba</item>
<item>sah</item>
<item>fo</item>
<item>nap</item>
<item>vls</item>
<item>sa</item>
<item>ce</item>
<item>qu</item>
<item>ku</item>
<item>min</item>
<item>bcl</item>
<item>ilo</item>
<item>ht</item>
<item>li</item>
<item>wa</item>
<item>vo</item>
<item>nds-nl</item>
<item>pam</item>
<item>new</item>
<item>mai</item>
<item>sn</item>
<item>pms</item>
<item>eml</item>
<item>yo</item>
<item>ha</item>
<item>gn</item>
<item>frr</item>
<item>gd</item>
<item>hsb</item>
<item>cv</item>
<item>lo</item>
<item>os</item>
<item>se</item>
<item>cdo</item>
<item>sd</item>
<item>ksh</item>
<item>bat-smg</item>
<item>bo</item>
<item>nah</item>
<item>xmf</item>
<item>ace</item>
<item>roa-tara</item>
<item>hak</item>
<item>bjn</item>
<item>gv</item>
<item>mt</item>
<item>pfl</item>
<item>szl</item>
<item>bpy</item>
<item>rue</item>
<item>co</item>
<item>diq</item>
<item>sc</item>
<item>rw</item>
<item>vep</item>
<item>lij</item>
<item>kw</item>
<item>fur</item>
<item>pcd</item>
<item>lad</item>
<item>tpi</item>
<item>ext</item>
<item>csb</item>
<item>rm</item>
<item>kab</item>
<item>gom</item>
<item>udm</item>
<item>mhr</item>
<item>glk</item>
<item>za</item>
<item>pdc</item>
<item>om</item>
<item>iu</item>
<item>nv</item>
<item>mi</item>
<item>nrm</item>
<item>tcy</item>
<item>frp</item>
<item>myv</item>
<item>kbp</item>
<item>dsb</item>
<item>zu</item>
<item>ln</item>
<item>mwl</item>
<item>fiu-vro</item>
<item>tum</item>
<item>tet</item>
<item>tn</item>
<item>pnt</item>
<item>stq</item>
<item>nov</item>
<item>ny</item>
<item>xh</item>
<item>crh</item>
<item>lfn</item>
<item>st</item>
<item>pap</item>
<item>ay</item>
<item>zea</item>
<item>bxr</item>
<item>kl</item>
<item>sm</item>
<item>ak</item>
<item>ve</item>
<item>pag</item>
<item>nso</item>
<item>kaa</item>
<item>lez</item>
<item>gag</item>
<item>kv</item>
<item>bm</item>
<item>to</item>
<item>lbe</item>
<item>krc</item>
<item>jam</item>
<item>ss</item>
<item>roa-rup</item>
<item>dv</item>
<item>ie</item>
<item>av</item>
<item>cbk-zam</item>
<item>chy</item>
<item>inh</item>
<item>ug</item>
<item>ch</item>
<item>arc</item>
<item>pih</item>
<item>mrj</item>
<item>kg</item>
<item>rmy</item>
<item>dty</item>
<item>na</item>
<item>ts</item>
<item>xal</item>
<item>wo</item>
<item>fj</item>
<item>tyv</item>
<item>olo</item>
<item>ltg</item>
<item>ff</item>
<item>jbo</item>
<item>haw</item>
<item>ki</item>
<item>chr</item>
<item>sg</item>
<item>atj</item>
<item>sat</item>
<item>ady</item>
<item>ty</item>
<item>lrc</item>
<item>ti</item>
<item>din</item>
<item>gor</item>
<item>lg</item>
<item>rn</item>
<item>bi</item>
<item>cu</item>
<item>kbd</item>
<item>pi</item>
<item>cr</item>
<item>koi</item>
<item>ik</item>
<item>mdf</item>
<item>bug</item>
<item>ee</item>
<item>shn</item>
<item>tw</item>
<item>dz</item>
<item>srn</item>
<item>ks</item>
<item>test</item>
<item>en-x-piglatin</item>
<item>ab</item>
</string-array>
<string-array name="preference_language_local_names">
<item>English</item>
<item>español</item>
<item>Deutsch</item>
<item>日本語</item>
<item>français</item>
<item>русский</item>
<item>português</item>
<item>italiano</item>
<item>简体中文</item>
<item>繁體中文</item>
<item>العربية</item>
<item>한국어</item>
<item>Bahasa Indonesia</item>
<item>polski</item>
<item>Nederlands</item>
<item>فارسی</item>
<item>हिन्दी</item>
<item>ไทย</item>
<item>Tiếng Việt</item>
<item>svenska</item>
<item>українська</item>
<item>čeština</item>
<item>Simple English</item>
<item>magyar</item>
<item>română</item>
<item>suomi</item>
<item>Ελληνικά</item>
<item>עברית</item>
<item>norsk</item>
<item>dansk</item>
<item>српски / srpski</item>
<item>hrvatski</item>
<item>Bahasa Melayu</item>
<item>български</item>
<item>català</item>
<item>Türkçe</item>
<item>slovenčina</item>
<item>srpskohrvatski / српскохрватски</item>
<item>বাংলা</item>
<item>Tagalog</item>
<item>मराठी</item>
<item>தமிழ்</item>
<item>қазақша</item>
<item>lietuvių</item>
<item>azərbaycanca</item>
<item>bosanski</item>
<item>slovenščina</item>
<item>shqip</item>
<item>مصرى</item>
<item>粵語</item>
<item>ქართული</item>
<item>తెలుగు</item>
<item>eesti</item>
<item>latviešu</item>
<item>മലയാളം</item>
<item>հայերեն</item>
<item>oʻzbekcha/ўзбекча</item>
<item>ಕನ್ನಡ</item>
<item>Afrikaans</item>
<item>norsk nynorsk</item>
<item>македонски</item>
<item>galego</item>
<item>Kiswahili</item>
<item>euskara</item>
<item>اردو</item>
<item>Кыргызча</item>
<item>ગુજરાતી</item>
<item>भोजपुरी</item>
<item>Scots</item>
<item>asturianu</item>
<item>íslenska</item>
<item>монгол</item>
<item>беларуская</item>
<item>aragonés</item>
<item>ភាសាខ្មែរ</item>
<item>සිංහල</item>
<item>Cebuano</item>
<item>Basa Jawa</item>
<item>Esperanto</item>
<item>Alemannisch</item>
<item>Igbo</item>
<item>Basa Sunda</item>
<item>беларуская (тарашкевіца)</item>
<item>Latina</item>
<item>မြန်မာဘာသာ</item>
<item>Cymraeg</item>
<item>नेपाली</item>
<item>Boarisch</item>
<item>تۆرکجه</item>
<item>مازِرونی</item>
<item>অসমীয়া</item>
<item>አማርኛ</item>
<item>Soomaaliga</item>
<item>ਪੰਜਾਬੀ</item>
<item>Basa Banyumasan</item>
<item>sicilianu</item>
<item>тоҷикӣ</item>
<item>کوردی</item>
<item>Gaeilge</item>
<item>Lëtzebuergesch</item>
<item>Winaray</item>
<item>Bân-lâm-gú</item>
<item>Plattdüütsch</item>
<item>Frysk</item>
<item>vèneto</item>
<item>پنجابی</item>
<item>文言</item>
<item>lumbaart</item>
<item>татарча/tatarça</item>
<item>Ido</item>
<item>interlingua</item>
<item>brezhoneg</item>
<item>Fiji Hindi</item>
<item>Malagasy</item>
<item>吴语</item>
<item>贛語</item>
<item>Ænglisc</item>
<item>ଓଡ଼ିଆ</item>
<item>occitan</item>
<item>ייִדיש</item>
<item>پښتو</item>
<item>Türkmençe</item>
<item>башҡортса</item>
<item>саха тыла</item>
<item>føroyskt</item>
<item>Napulitano</item>
<item>West-Vlams</item>
<item>संस्कृतम्</item>
<item>нохчийн</item>
<item>Runa Simi</item>
<item>kurdî</item>
<item>Baso Minangkabau</item>
<item>Bikol Central</item>
<item>Ilokano</item>
<item>Kreyòl ayisyen</item>
<item>Limburgs</item>
<item>walon</item>
<item>Volapük</item>
<item>Nedersaksies</item>
<item>Kapampangan</item>
<item>नेपाल भाषा</item>
<item>मैथिली</item>
<item>chiShona</item>
<item>Piemontèis</item>
<item>emiliàn e rumagnòl</item>
<item>Yorùbá</item>
<item>Hausa</item>
<item>Avañe\'ẽ</item>
<item>Nordfriisk</item>
<item>Gàidhlig</item>
<item>hornjoserbsce</item>
<item>Чӑвашла</item>
<item>ລາວ</item>
<item>Ирон</item>
<item>davvisámegiella</item>
<item>Mìng-dĕ̤ng-ngṳ̄</item>
<item>سنڌي</item>
<item>Ripoarisch</item>
<item>žemaitėška</item>
<item>བོད་ཡིག</item>
<item>Nāhuatl</item>
<item>მარგალური</item>
<item>Acèh</item>
<item>tarandíne</item>
<item>客家語/Hak-kâ-ngî</item>
<item>Bahasa Banjar</item>
<item>Gaelg</item>
<item>Malti</item>
<item>Pälzisch</item>
<item>ślůnski</item>
<item>বিষ্ণুপ্রিয়া মণিপুরী</item>
<item>русиньскый</item>
<item>corsu</item>
<item>Zazaki</item>
<item>sardu</item>
<item>Kinyarwanda</item>
<item>vepsän kel</item>
<item>Ligure</item>
<item>kernowek</item>
<item>furlan</item>
<item>Picard</item>
<item>Ladino</item>
<item>Tok Pisin</item>
<item>estremeñu</item>
<item>kaszëbsczi</item>
<item>rumantsch</item>
<item>Taqbaylit</item>
<item>गोंयची कोंकणी / Gõychi Konknni</item>
<item>удмурт</item>
<item>олык марий</item>
<item>گیلکی</item>
<item>Vahcuengh</item>
<item>Deitsch</item>
<item>Oromoo</item>
<item>ᐃᓄᒃᑎᑐᑦ/inuktitut</item>
<item>Diné bizaad</item>
<item>Māori</item>
<item>Nouormand</item>
<item>ತುಳು</item>
<item>arpetan</item>
<item>эрзянь</item>
<item>Kabɩ</item>
<item>dolnoserbski</item>
<item>isiZulu</item>
<item>lingála</item>
<item>Mirandés</item>
<item>Võro</item>
<item>chiTumbuka</item>
<item>tetun</item>
<item>Setswana</item>
<item>Ποντιακά</item>
<item>Seeltersk</item>
<item>Novial</item>
<item>Chi-Chewa</item>
<item>isiXhosa</item>
<item>qırımtatarca</item>
<item>Lingua Franca Nova</item>
<item>Sesotho</item>
<item>Papiamentu</item>
<item>Aymar aru</item>
<item>Zeêuws</item>
<item>буряад</item>
<item>kalaallisut</item>
<item>Gagana Samoa</item>
<item>Akan</item>
<item>Tshivenda</item>
<item>Pangasinan</item>
<item>Sesotho sa Leboa</item>
<item>Qaraqalpaqsha</item>
<item>лезги</item>
<item>Gagauz</item>
<item>коми</item>
<item>bamanankan</item>
<item>lea faka-Tonga</item>
<item>лакку</item>
<item>къарачай-малкъар</item>
<item>Patois</item>
<item>SiSwati</item>
<item>armãneashti</item>
<item>ދިވެހިބަސް</item>
<item>Interlingue</item>
<item>авар</item>
<item>Chavacano de Zamboanga</item>
<item>Tsetsêhestâhese</item>
<item>ГӀалгӀай</item>
<item>ئۇيغۇرچە / Uyghurche</item>
<item>Chamoru</item>
<item>ܐܪܡܝܐ</item>
<item>Norfuk / Pitkern</item>
<item>кырык мары</item>
<item>Kongo</item>
<item>Romani</item>
<item>डोटेली</item>
<item>Dorerin Naoero</item>
<item>Xitsonga</item>
<item>хальмг</item>
<item>Wolof</item>
<item>Na Vosa Vakaviti</item>
<item>тыва дыл</item>
<item>Livvinkarjala</item>
<item>latgaļu</item>
<item>Fulfulde</item>
<item>la .lojban.</item>
<item>Hawaiʻi</item>
<item>Gĩkũyũ</item>
<item>ᏣᎳᎩ</item>
<item>Sängö</item>
<item>Atikamekw</item>
<item>ᱥᱟᱱᱛᱟᱲᱤ</item>
<item>адыгабзэ</item>
<item>reo tahiti</item>
<item>لۊری شومالی</item>
<item>ትግርኛ</item>
<item>Thuɔŋjäŋ</item>
<item>Bahasa Hulontalo</item>
<item>Luganda</item>
<item>Kirundi</item>
<item>Bislama</item>
<item>словѣньскъ / ⰔⰎⰑⰂⰡⰐⰠⰔⰍⰟ</item>
<item>Адыгэбзэ</item>
<item>पालि</item>
<item>Nēhiyawēwin / ᓀᐦᐃᔭᐍᐏᐣ</item>
<item>Перем Коми</item>
<item>Iñupiak</item>
<item>мокшень</item>
<item>ᨅᨔ ᨕᨘᨁᨗ</item>
<item>eʋegbe</item>
<item>ၽႃႇသႃႇတႆး </item>
<item>Twi</item>
<item>ཇོང་ཁ</item>
<item>Sranantongo</item>
<item>कॉशुर / کٲشُر</item>
<item>Test</item>
<item>Igpay Atinlay</item>
<item>Аҧсшәа</item>
</string-array>
<string-array name="preference_language_canonical_names">
<item>English</item>
<item>Spanish</item>
<item>German</item>
<item>Japanese</item>
<item>French</item>
<item>Russian</item>
<item>Portuguese</item>
<item>Italian</item>
<item>Simplified Chinese</item>
<item>Traditional Chinese</item>
<item>Arabic</item>
<item>Korean</item>
<item>Indonesian</item>
<item>Polish</item>
<item>Dutch</item>
<item>Persian</item>
<item>Hindi</item>
<item>Thai</item>
<item>Vietnamese</item>
<item>Swedish</item>
<item>Ukrainian</item>
<item>Czech</item>
<item>Simple English</item>
<item>Hungarian</item>
<item>Romanian</item>
<item>Finnish</item>
<item>Greek</item>
<item>Hebrew</item>
<item>Norwegian</item>
<item>Danish</item>
<item>Serbian</item>
<item>Croatian</item>
<item>Malay</item>
<item>Bulgarian</item>
<item>Catalan</item>
<item>Turkish</item>
<item>Slovak</item>
<item>Serbo-Croatian</item>
<item>Bangla</item>
<item>Tagalog</item>
<item>Marathi</item>
<item>Tamil</item>
<item>Kazakh</item>
<item>Lithuanian</item>
<item>Azerbaijani</item>
<item>Bosnian</item>
<item>Slovenian</item>
<item>Albanian</item>
<item>Egyptian Arabic</item>
<item>Cantonese</item>
<item>Georgian</item>
<item>Telugu</item>
<item>Estonian</item>
<item>Latvian</item>
<item>Malayalam</item>
<item>Armenian</item>
<item>Uzbek</item>
<item>Kannada</item>
<item>Afrikaans</item>
<item>Norwegian Nynorsk</item>
<item>Macedonian</item>
<item>Galician</item>
<item>Swahili</item>
<item>Basque</item>
<item>Urdu</item>
<item>Kyrgyz</item>
<item>Gujarati</item>
<item>Bhojpuri</item>
<item>Scots</item>
<item>Asturian</item>
<item>Icelandic</item>
<item>Mongolian</item>
<item>Belarusian</item>
<item>Aragonese</item>
<item>Khmer</item>
<item>Sinhala</item>
<item>Cebuano</item>
<item>Javanese</item>
<item>Esperanto</item>
<item>Alemannisch</item>
<item>Igbo</item>
<item>Sundanese</item>
<item>Belarusian (Taraškievica orthography)</item>
<item>Latin</item>
<item>Burmese</item>
<item>Welsh</item>
<item>Nepali</item>
<item>Bavarian</item>
<item>South Azerbaijani</item>
<item>Mazanderani</item>
<item>Assamese</item>
<item>Amharic</item>
<item>Somali</item>
<item>Punjabi</item>
<item>Basa Banyumasan</item>
<item>Sicilian</item>
<item>Tajik</item>
<item>Central Kurdish</item>
<item>Irish</item>
<item>Luxembourgish</item>
<item>Waray</item>
<item>Chinese (Min Nan)</item>
<item>Low German</item>
<item>Western Frisian</item>
<item>Venetian</item>
<item>Western Punjabi</item>
<item>Classical Chinese</item>
<item>Lombard</item>
<item>Tatar</item>
<item>Ido</item>
<item>Interlingua</item>
<item>Breton</item>
<item>Fiji Hindi</item>
<item>Malagasy</item>
<item>Wu Chinese</item>
<item>Gan Chinese</item>
<item>Old English</item>
<item>Odia</item>
<item>Occitan</item>
<item>Yiddish</item>
<item>Pashto</item>
<item>Turkmen</item>
<item>Bashkir</item>
<item>Sakha</item>
<item>Faroese</item>
<item>Neapolitan</item>
<item>West Flemish</item>
<item>Sanskrit</item>
<item>Chechen</item>
<item>Quechua</item>
<item>Kurdish</item>
<item>Minangkabau</item>
<item>Central Bikol</item>
<item>Iloko</item>
<item>Haitian Creole</item>
<item>Limburgish</item>
<item>Walloon</item>
<item>Volapük</item>
<item>Low Saxon</item>
<item>Pampanga</item>
<item>Newari</item>
<item>Maithili</item>
<item>Shona</item>
<item>Piedmontese</item>
<item>Emiliano-Romagnolo</item>
<item>Yoruba</item>
<item>Hausa</item>
<item>Guarani</item>
<item>Northern Frisian</item>
<item>Scottish Gaelic</item>
<item>Upper Sorbian</item>
<item>Chuvash</item>
<item>Lao</item>
<item>Ossetic</item>
<item>Northern Sami</item>
<item>Min Dong Chinese</item>
<item>Sindhi</item>
<item>Colognian</item>
<item>Samogitian</item>
<item>Tibetan</item>
<item>Nāhuatl</item>
<item>Mingrelian</item>
<item>Achinese</item>
<item>Tarantino</item>
<item>Hakka Chinese</item>
<item>Banjar</item>
<item>Manx</item>
<item>Maltese</item>
<item>Palatine German</item>
<item>Silesian</item>
<item>Bishnupriya</item>
<item>Rusyn</item>
<item>Corsican</item>
<item>Zazaki</item>
<item>Sardinian</item>
<item>Kinyarwanda</item>
<item>Veps</item>
<item>Ligurian</item>
<item>Cornish</item>
<item>Friulian</item>
<item>Picard</item>
<item>Ladino</item>
<item>Tok Pisin</item>
<item>Extremaduran</item>
<item>Kashubian</item>
<item>Romansh</item>
<item>Kabyle</item>
<item>Goan Konkani</item>
<item>Udmurt</item>
<item>Eastern Mari</item>
<item>Gilaki</item>
<item>Zhuang</item>
<item>Pennsylvania German</item>
<item>Oromo</item>
<item>Inuktitut</item>
<item>Navajo</item>
<item>Maori</item>
<item>Norman</item>
<item>Tulu</item>
<item>Arpitan</item>
<item>Erzya</item>
<item>Kabiye</item>
<item>Lower Sorbian</item>
<item>Zulu</item>
<item>Lingala</item>
<item>Mirandese</item>
<item>Võro</item>
<item>Tumbuka</item>
<item>Tetum</item>
<item>Tswana</item>
<item>Pontic</item>
<item>Saterland Frisian</item>
<item>Novial</item>
<item>Nyanja</item>
<item>Xhosa</item>
<item>Crimean Turkish</item>
<item>Lingua Franca Nova</item>
<item>Southern Sotho</item>
<item>Papiamento</item>
<item>Aymara</item>
<item>Zeelandic</item>
<item>Russia Buriat</item>
<item>Kalaallisut</item>
<item>Samoan</item>
<item>Akan</item>
<item>Venda</item>
<item>Pangasinan</item>
<item>Northern Sotho</item>
<item>Kara-Kalpak</item>
<item>Lezghian</item>
<item>Gagauz</item>
<item>Komi</item>
<item>Bambara</item>
<item>Tongan</item>
<item>Lak</item>
<item>Karachay-Balkar</item>
<item>Jamaican Creole English</item>
<item>Swati</item>
<item>Aromanian</item>
<item>Divehi</item>
<item>Interlingue</item>
<item>Avaric</item>
<item>Chavacano</item>
<item>Cheyenne</item>
<item>Ingush</item>
<item>Uyghur</item>
<item>Chamorro</item>
<item>Aramaic</item>
<item>Norfuk / Pitkern</item>
<item>Western Mari</item>
<item>Kongo</item>
<item>Romani</item>
<item>Doteli</item>
<item>Nauru</item>
<item>Tsonga</item>
<item>Kalmyk</item>
<item>Wolof</item>
<item>Fijian</item>
<item>Tuvinian</item>
<item>Livvi-Karelian</item>
<item>Latgalian</item>
<item>Fulah</item>
<item>Lojban</item>
<item>Hawaiian</item>
<item>Kikuyu</item>
<item>Cherokee</item>
<item>Sango</item>
<item>Atikamekw</item>
<item>Santali</item>
<item>Adyghe</item>
<item>Tahitian</item>
<item>Northern Luri</item>
<item>Tigrinya</item>
<item>Dinka</item>
<item>Gorontalo</item>
<item>Ganda</item>
<item>Rundi</item>
<item>Bislama</item>
<item>Church Slavic</item>
<item>Kabardian</item>
<item>Pali</item>
<item>Cree</item>
<item>Komi-Permyak</item>
<item>Inupiaq</item>
<item>Moksha</item>
<item>Buginese</item>
<item>Ewe</item>
<item>Shan</item>
<item>Twi</item>
<item>Dzongkha</item>
<item>Sranan Tongo</item>
<item>Kashmiri</item>
<item>Test</item>
<item>Pig Latin</item>
<item>Abkhazian</item>
</string-array>
</resources>