Skip to content

Commit 28a6a3b

Browse files
author
maskara
committed
Integrate notifications API
1 parent d78c7be commit 28a6a3b

32 files changed

+1013
-58
lines changed

app/build.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ dependencies {
4646

4747
implementation 'com.facebook.fresco:fresco:1.5.0'
4848
implementation 'com.facebook.stetho:stetho:1.5.0'
49+
implementation "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
50+
implementation "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
51+
implementation 'org.apache.commons:commons-lang3:3.5'
4952

5053
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
5154
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
@@ -118,6 +121,7 @@ android {
118121
productFlavors {
119122
prod {
120123
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
124+
buildConfigField "String", "COMMONS_BASE_URL", "\"https://commons.wikimedia.org\""
121125
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
122126
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
123127
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
@@ -132,6 +136,7 @@ android {
132136
beta {
133137
// What values do we need to hit the BETA versions of the site / api ?
134138
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
139+
buildConfigField "String", "COMMONS_BASE_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
135140
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
136141
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
137142
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""

app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import dagger.Module;
1414
import dagger.Provides;
1515
import fr.free.nrw.commons.BuildConfig;
16+
import fr.free.nrw.commons.CommonsApplication;
1617
import fr.free.nrw.commons.auth.AccountUtil;
1718
import fr.free.nrw.commons.auth.SessionManager;
1819
import fr.free.nrw.commons.caching.CacheController;
@@ -21,6 +22,7 @@
2122
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
2223
import fr.free.nrw.commons.mwapi.MediaWikiApi;
2324
import fr.free.nrw.commons.nearby.NearbyPlaces;
25+
import fr.free.nrw.commons.notification.NotificationClient;
2426
import fr.free.nrw.commons.upload.UploadController;
2527

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

38+
private CommonsApplication application;
3539
private Context applicationContext;
3640

3741
public CommonsApplicationModule(Context applicationContext) {
@@ -100,8 +104,8 @@ public SessionManager providesSessionManager(Context context,
100104

101105
@Provides
102106
@Singleton
103-
public MediaWikiApi provideMediaWikiApi() {
104-
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
107+
public MediaWikiApi provideMediaWikiApi(Context context) {
108+
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST);
105109
}
106110

107111
@Provides
@@ -133,4 +137,10 @@ public NearbyPlaces provideNearbyPlaces() {
133137
public LruCache<String, String> provideLruCache() {
134138
return new LruCache<>(1024);
135139
}
136-
}
140+
141+
@Provides
142+
@Singleton
143+
public NotificationClient provideNotificationClient() {
144+
return new NotificationClient(BuildConfig.COMMONS_BASE_URL);
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package fr.free.nrw.commons.json;
2+
3+
import android.support.annotation.NonNull;
4+
import android.support.annotation.Nullable;
5+
6+
import com.google.gson.Gson;
7+
8+
import fr.free.nrw.commons.network.GsonUtil;
9+
10+
public final class GsonMarshaller {
11+
public static String marshal(@Nullable Object object) {
12+
return marshal(GsonUtil.getDefaultGson(), object);
13+
}
14+
15+
public static String marshal(@NonNull Gson gson, @Nullable Object object) {
16+
return gson.toJson(object);
17+
}
18+
19+
private GsonMarshaller() { }
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package fr.free.nrw.commons.json;
2+
3+
import android.support.annotation.NonNull;
4+
import android.support.annotation.Nullable;
5+
6+
import com.google.gson.Gson;
7+
import com.google.gson.reflect.TypeToken;
8+
9+
import fr.free.nrw.commons.network.GsonUtil;
10+
11+
public final class GsonUnmarshaller {
12+
/** @return Unmarshalled object. */
13+
public static <T> T unmarshal(Class<T> clazz, @Nullable String json) {
14+
return unmarshal(GsonUtil.getDefaultGson(), clazz, json);
15+
}
16+
17+
/** @return Unmarshalled collection of objects. */
18+
public static <T> T unmarshal(TypeToken<T> typeToken, @Nullable String json) {
19+
return unmarshal(GsonUtil.getDefaultGson(), typeToken, json);
20+
}
21+
22+
/** @return Unmarshalled object. */
23+
public static <T> T unmarshal(@NonNull Gson gson, Class<T> clazz, @Nullable String json) {
24+
return gson.fromJson(json, clazz);
25+
}
26+
27+
/** @return Unmarshalled collection of objects. */
28+
public static <T> T unmarshal(@NonNull Gson gson, TypeToken<T> typeToken, @Nullable String json) {
29+
// From the manual: "Fairly hideous... Unfortunately, no way to get around this in Java".
30+
return gson.fromJson(json, typeToken.getType());
31+
}
32+
33+
private GsonUnmarshaller() { }
34+
}
35+
36+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package fr.free.nrw.commons.json;
2+
3+
import android.support.annotation.NonNull;
4+
import android.support.annotation.Nullable;
5+
import android.support.v4.util.ArraySet;
6+
7+
import com.google.gson.Gson;
8+
import com.google.gson.JsonParseException;
9+
import com.google.gson.TypeAdapter;
10+
import com.google.gson.TypeAdapterFactory;
11+
import com.google.gson.reflect.TypeToken;
12+
import com.google.gson.stream.JsonReader;
13+
import com.google.gson.stream.JsonWriter;
14+
15+
16+
import java.io.IOException;
17+
import java.lang.reflect.Field;
18+
import java.util.Collections;
19+
import java.util.Set;
20+
21+
import fr.free.nrw.commons.json.annotations.Required;
22+
23+
/**
24+
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
25+
* missing fields annotated with @Required.
26+
*
27+
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
28+
* contain null elements after deserialization!
29+
*
30+
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
31+
* annotation and another corresponding TypeAdapter(Factory).
32+
*/
33+
class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory {
34+
@Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) {
35+
Class<?> rawType = typeToken.getRawType();
36+
Set<Field> requiredFields = collectRequiredFields(rawType);
37+
38+
if (requiredFields.isEmpty()) {
39+
return null;
40+
}
41+
42+
setFieldsAccessible(requiredFields, true);
43+
return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields);
44+
}
45+
46+
@NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) {
47+
Field[] fields = clazz.getDeclaredFields();
48+
Set<Field> required = new ArraySet<>();
49+
for (Field field : fields) {
50+
if (field.isAnnotationPresent(Required.class)) {
51+
required.add(field);
52+
}
53+
}
54+
return Collections.unmodifiableSet(required);
55+
}
56+
57+
private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) {
58+
for (Field field : fields) {
59+
field.setAccessible(accessible);
60+
}
61+
}
62+
63+
private static final class Adapter<T> extends TypeAdapter<T> {
64+
@NonNull private final TypeAdapter<T> delegate;
65+
@NonNull private final Set<Field> requiredFields;
66+
67+
private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) {
68+
this.delegate = delegate;
69+
this.requiredFields = requiredFields;
70+
}
71+
72+
@Override public void write(JsonWriter out, T value) throws IOException {
73+
delegate.write(out, value);
74+
}
75+
76+
@Override @Nullable public T read(JsonReader in) throws IOException {
77+
T deserialized = delegate.read(in);
78+
return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null;
79+
}
80+
81+
private boolean allRequiredFieldsPresent(@NonNull T deserialized,
82+
@NonNull Set<Field> required) {
83+
for (Field field : required) {
84+
try {
85+
if (field.get(deserialized) == null) {
86+
return false;
87+
}
88+
} catch (IllegalArgumentException | IllegalAccessException e) {
89+
throw new JsonParseException(e);
90+
}
91+
}
92+
return true;
93+
}
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package fr.free.nrw.commons.json;
2+
3+
import android.net.Uri;
4+
5+
import com.google.gson.TypeAdapter;
6+
import com.google.gson.stream.JsonReader;
7+
import com.google.gson.stream.JsonWriter;
8+
9+
import java.io.IOException;
10+
11+
public class UriTypeAdapter extends TypeAdapter<Uri> {
12+
@Override
13+
public void write(JsonWriter out, Uri value) throws IOException {
14+
out.value(value.toString());
15+
}
16+
17+
@Override
18+
public Uri read(JsonReader in) throws IOException {
19+
String url = in.nextString();
20+
return Uri.parse(url);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package fr.free.nrw.commons.json.annotations;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
import static java.lang.annotation.ElementType.FIELD;
9+
10+
/**
11+
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
12+
* an instantiated object.
13+
*
14+
* E.g.: @NonNull @Required private String title;
15+
*/
16+
@Documented
17+
@Retention(RetentionPolicy.RUNTIME)
18+
@Target(FIELD)
19+
public @interface Required {
20+
}
21+

app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package fr.free.nrw.commons.mwapi;
22

3+
import android.content.Context;
34
import android.os.Build;
45
import android.support.annotation.NonNull;
56
import android.support.annotation.Nullable;
@@ -21,6 +22,8 @@
2122
import org.apache.http.util.EntityUtils;
2223
import org.mediawiki.api.ApiResult;
2324
import org.mediawiki.api.MWApi;
25+
import org.w3c.dom.Node;
26+
import org.w3c.dom.NodeList;
2427

2528
import java.io.IOException;
2629
import java.io.InputStream;
@@ -36,11 +39,17 @@
3639

3740
import fr.free.nrw.commons.BuildConfig;
3841
import fr.free.nrw.commons.PageTitle;
42+
import fr.free.nrw.commons.notification.Notification;
3943
import in.yuvi.http.fluent.Http;
4044
import io.reactivex.Observable;
4145
import io.reactivex.Single;
4246
import timber.log.Timber;
4347

48+
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
49+
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult;
50+
import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType;
51+
import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification;
52+
4453
/**
4554
* @author Addshore
4655
*/
@@ -50,8 +59,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
5059
private static final String THUMB_SIZE = "640";
5160
private AbstractHttpClient httpClient;
5261
private MWApi api;
62+
private Context context;
5363

54-
public ApacheHttpClientMediaWikiApi(String apiURL) {
64+
public ApacheHttpClientMediaWikiApi(Context context, String apiURL) {
65+
this.context = context;
5566
BasicHttpParams params = new BasicHttpParams();
5667
SchemeRegistry schemeRegistry = new SchemeRegistry();
5768
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@@ -353,6 +364,42 @@ public String revisionsByFilename(String filename) throws IOException {
353364
.getString("/api/query/pages/page/revisions/rev");
354365
}
355366

367+
@Override
368+
@NonNull
369+
public List<Notification> getNotifications() {
370+
ApiResult notificationNode = null;
371+
try {
372+
notificationNode = api.action("query")
373+
.param("notprop", "list")
374+
.param("format", "xml")
375+
.param("meta", "notifications")
376+
.param("notfilter", "!read")
377+
.get()
378+
.getNode("/api/query/notifications/list");
379+
} catch (IOException e) {
380+
Timber.e("Failed to obtain searchCategories", e);
381+
}
382+
383+
if (notificationNode == null) {
384+
return new ArrayList<>();
385+
}
386+
387+
List<Notification> notifications = new ArrayList<>();
388+
389+
NodeList childNodes = notificationNode.getDocument().getChildNodes();
390+
391+
for (int i = 0; i < childNodes.getLength(); i++) {
392+
Node node = childNodes.item(i);
393+
if (isCommonsNotification(node)
394+
&& !getNotificationType(node).equals(UNKNOWN)) {
395+
notifications.add(getNotificationFromApiResult(context, node));
396+
}
397+
}
398+
399+
return notifications;
400+
}
401+
402+
356403
@Override
357404
public boolean existingFile(String fileSha1) throws IOException {
358405
return api.action("query")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package fr.free.nrw.commons.mwapi;
2+
3+
import org.apache.commons.lang3.builder.EqualsBuilder;
4+
import org.apache.commons.lang3.builder.HashCodeBuilder;
5+
import org.apache.commons.lang3.builder.ToStringBuilder;
6+
7+
public abstract class BaseModel {
8+
@Override public String toString() {
9+
return ToStringBuilder.reflectionToString(this);
10+
}
11+
12+
@Override public int hashCode() {
13+
return HashCodeBuilder.reflectionHashCode(this);
14+
}
15+
16+
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
17+
@Override public boolean equals(Object other) {
18+
return EqualsBuilder.reflectionEquals(this, other);
19+
}
20+
}

0 commit comments

Comments
 (0)