Skip to content

Commit

Permalink
Integrate notifications API
Browse files Browse the repository at this point in the history
  • Loading branch information
maskara committed Jan 21, 2018
1 parent d78c7be commit 28a6a3b
Show file tree
Hide file tree
Showing 32 changed files with 1,013 additions and 58 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ dependencies {

implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0'
implementation "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
implementation "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
implementation 'org.apache.commons:commons-lang3:3.5'

implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
Expand Down Expand Up @@ -118,6 +121,7 @@ android {
productFlavors {
prod {
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "COMMONS_BASE_URL", "\"https://commons.wikimedia.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
Expand All @@ -132,6 +136,7 @@ android {
beta {
// What values do we need to hit the BETA versions of the site / api ?
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "COMMONS_BASE_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
Expand All @@ -21,6 +22,7 @@
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.notification.NotificationClient;
import fr.free.nrw.commons.upload.UploadController;

import static android.content.Context.MODE_PRIVATE;
Expand All @@ -31,7 +33,9 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;

private CommonsApplication application;
private Context applicationContext;

public CommonsApplicationModule(Context applicationContext) {
Expand Down Expand Up @@ -100,8 +104,8 @@ public SessionManager providesSessionManager(Context context,

@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
public MediaWikiApi provideMediaWikiApi(Context context) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST);
}

@Provides
Expand Down Expand Up @@ -133,4 +137,10 @@ public NearbyPlaces provideNearbyPlaces() {
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
}

@Provides
@Singleton
public NotificationClient provideNotificationClient() {
return new NotificationClient(BuildConfig.COMMONS_BASE_URL);
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/json/GsonMarshaller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.free.nrw.commons.json;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.google.gson.Gson;

import fr.free.nrw.commons.network.GsonUtil;

public final class GsonMarshaller {
public static String marshal(@Nullable Object object) {
return marshal(GsonUtil.getDefaultGson(), object);
}

public static String marshal(@NonNull Gson gson, @Nullable Object object) {
return gson.toJson(object);
}

private GsonMarshaller() { }
}
36 changes: 36 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/json/GsonUnmarshaller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package fr.free.nrw.commons.json;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import fr.free.nrw.commons.network.GsonUtil;

public final class GsonUnmarshaller {
/** @return Unmarshalled object. */
public static <T> T unmarshal(Class<T> clazz, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), clazz, json);
}

/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(TypeToken<T> typeToken, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), typeToken, json);
}

/** @return Unmarshalled object. */
public static <T> T unmarshal(@NonNull Gson gson, Class<T> clazz, @Nullable String json) {
return gson.fromJson(json, clazz);
}

/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(@NonNull Gson gson, TypeToken<T> typeToken, @Nullable String json) {
// From the manual: "Fairly hideous... Unfortunately, no way to get around this in Java".
return gson.fromJson(json, typeToken.getType());
}

private GsonUnmarshaller() { }
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package fr.free.nrw.commons.json;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.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 java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Set;

import fr.free.nrw.commons.json.annotations.Required;

/**
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
* missing fields annotated with @Required.
*
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
* contain null elements after deserialization!
*
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
* annotation and another corresponding TypeAdapter(Factory).
*/
class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory {
@Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) {
Class<?> rawType = typeToken.getRawType();
Set<Field> requiredFields = collectRequiredFields(rawType);

if (requiredFields.isEmpty()) {
return null;
}

setFieldsAccessible(requiredFields, true);
return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields);
}

@NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
Set<Field> required = new ArraySet<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Required.class)) {
required.add(field);
}
}
return Collections.unmodifiableSet(required);
}

private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) {
for (Field field : fields) {
field.setAccessible(accessible);
}
}

private static final class Adapter<T> extends TypeAdapter<T> {
@NonNull private final TypeAdapter<T> delegate;
@NonNull private final Set<Field> requiredFields;

private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) {
this.delegate = delegate;
this.requiredFields = requiredFields;
}

@Override public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}

@Override @Nullable public T read(JsonReader in) throws IOException {
T deserialized = delegate.read(in);
return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null;
}

private boolean allRequiredFieldsPresent(@NonNull T deserialized,
@NonNull Set<Field> required) {
for (Field field : required) {
try {
if (field.get(deserialized) == null) {
return false;
}
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new JsonParseException(e);
}
}
return true;
}
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/json/UriTypeAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fr.free.nrw.commons.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fr.free.nrw.commons.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 {
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package fr.free.nrw.commons.mwapi;

import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand All @@ -21,6 +22,8 @@
import org.apache.http.util.EntityUtils;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -36,11 +39,17 @@

import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.notification.Notification;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;

import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;

/**
* @author Addshore
*/
Expand All @@ -50,8 +59,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
private Context context;

public ApacheHttpClientMediaWikiApi(String apiURL) {
public ApacheHttpClientMediaWikiApi(Context context, String apiURL) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
Expand Down Expand Up @@ -353,6 +364,42 @@ public String revisionsByFilename(String filename) throws IOException {
.getString("/api/query/pages/page/revisions/rev");
}

@Override
@NonNull
public List<Notification> getNotifications() {
ApiResult notificationNode = null;
try {
notificationNode = api.action("query")
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
.param("notfilter", "!read")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}

if (notificationNode == null) {
return new ArrayList<>();
}

List<Notification> notifications = new ArrayList<>();

NodeList childNodes = notificationNode.getDocument().getChildNodes();

for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)) {
notifications.add(getNotificationFromApiResult(context, node));
}
}

return notifications;
}


@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/mwapi/BaseModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.free.nrw.commons.mwapi;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

public abstract class BaseModel {
@Override public String toString() {
return ToStringBuilder.reflectionToString(this);
}

@Override public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@Override public boolean equals(Object other) {
return EqualsBuilder.reflectionEquals(this, other);
}
}
Loading

0 comments on commit 28a6a3b

Please sign in to comment.