mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Integrate notifications API
This commit is contained in:
parent
d78c7befbd
commit
28a6a3b71d
32 changed files with 1013 additions and 58 deletions
|
|
@ -46,6 +46,9 @@ dependencies {
|
||||||
|
|
||||||
implementation 'com.facebook.fresco:fresco:1.5.0'
|
implementation 'com.facebook.fresco:fresco:1.5.0'
|
||||||
implementation 'com.facebook.stetho:stetho: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:$DAGGER_VERSION"
|
||||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
||||||
|
|
@ -118,6 +121,7 @@ android {
|
||||||
productFlavors {
|
productFlavors {
|
||||||
prod {
|
prod {
|
||||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
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", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
||||||
|
|
@ -132,6 +136,7 @@ android {
|
||||||
beta {
|
beta {
|
||||||
// What values do we need to hit the BETA versions of the site / api ?
|
// 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", "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", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
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.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.caching.CacheController;
|
import fr.free.nrw.commons.caching.CacheController;
|
||||||
|
|
@ -21,6 +22,7 @@ import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
||||||
|
import fr.free.nrw.commons.notification.NotificationClient;
|
||||||
import fr.free.nrw.commons.upload.UploadController;
|
import fr.free.nrw.commons.upload.UploadController;
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
import static android.content.Context.MODE_PRIVATE;
|
||||||
|
|
@ -31,7 +33,9 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public class CommonsApplicationModule {
|
public class CommonsApplicationModule {
|
||||||
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
|
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;
|
private Context applicationContext;
|
||||||
|
|
||||||
public CommonsApplicationModule(Context applicationContext) {
|
public CommonsApplicationModule(Context applicationContext) {
|
||||||
|
|
@ -100,8 +104,8 @@ public class CommonsApplicationModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public MediaWikiApi provideMediaWikiApi() {
|
public MediaWikiApi provideMediaWikiApi(Context context) {
|
||||||
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
|
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -133,4 +137,10 @@ public class CommonsApplicationModule {
|
||||||
public LruCache<String, String> provideLruCache() {
|
public LruCache<String, String> provideLruCache() {
|
||||||
return new LruCache<>(1024);
|
return new LruCache<>(1024);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
public NotificationClient provideNotificationClient() {
|
||||||
|
return new NotificationClient(BuildConfig.COMMONS_BASE_URL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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() { }
|
||||||
|
}
|
||||||
|
|
@ -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() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.mwapi;
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
@ -21,6 +22,8 @@ import org.apache.http.params.CoreProtocolPNames;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.mediawiki.api.ApiResult;
|
import org.mediawiki.api.ApiResult;
|
||||||
import org.mediawiki.api.MWApi;
|
import org.mediawiki.api.MWApi;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -36,11 +39,17 @@ import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.PageTitle;
|
import fr.free.nrw.commons.PageTitle;
|
||||||
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import in.yuvi.http.fluent.Http;
|
import in.yuvi.http.fluent.Http;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import timber.log.Timber;
|
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
|
* @author Addshore
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,8 +59,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
private static final String THUMB_SIZE = "640";
|
private static final String THUMB_SIZE = "640";
|
||||||
private AbstractHttpClient httpClient;
|
private AbstractHttpClient httpClient;
|
||||||
private MWApi api;
|
private MWApi api;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
public ApacheHttpClientMediaWikiApi(String apiURL) {
|
public ApacheHttpClientMediaWikiApi(Context context, String apiURL) {
|
||||||
|
this.context = context;
|
||||||
BasicHttpParams params = new BasicHttpParams();
|
BasicHttpParams params = new BasicHttpParams();
|
||||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||||
|
|
@ -353,6 +364,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
||||||
.getString("/api/query/pages/page/revisions/rev");
|
.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
|
@Override
|
||||||
public boolean existingFile(String fileSha1) throws IOException {
|
public boolean existingFile(String fileSha1) throws IOException {
|
||||||
return api.action("query")
|
return api.action("query")
|
||||||
|
|
|
||||||
20
app/src/main/java/fr/free/nrw/commons/mwapi/BaseModel.java
Normal file
20
app/src/main/java/fr/free/nrw/commons/mwapi/BaseModel.java
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,9 @@ import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.notification.Notification;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
|
|
@ -43,6 +45,9 @@ public interface MediaWikiApi {
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
List<Notification> getNotifications() throws IOException;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||||
|
|
||||||
|
|
@ -51,6 +56,8 @@ public interface MediaWikiApi {
|
||||||
|
|
||||||
boolean existingFile(String fileSha1) throws IOException;
|
boolean existingFile(String fileSha1) throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MwQueryResponse<T> extends MwResponse {
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") @SerializedName("batchcomplete") private boolean batchComplete;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") @SerializedName("continue") @Nullable
|
||||||
|
private Map<String, String> continuation;
|
||||||
|
|
||||||
|
@Nullable private T query;
|
||||||
|
|
||||||
|
public boolean batchComplete() {
|
||||||
|
return batchComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public Map<String, String> continuation() {
|
||||||
|
return continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public T query() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean success() {
|
||||||
|
return super.success() && query != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected void setQuery(@Nullable T query) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/src/main/java/fr/free/nrw/commons/mwapi/MwResponse.java
Normal file
46
app/src/main/java/fr/free/nrw/commons/mwapi/MwResponse.java
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class MwResponse extends BaseModel {
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private MwServiceError error;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") @Nullable private Map<String, Warning> warnings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @SerializedName("servedby") @NonNull
|
||||||
|
private String servedBy;
|
||||||
|
|
||||||
|
@Nullable public MwServiceError getError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasError() {
|
||||||
|
return error != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return error == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public String code() {
|
||||||
|
return error != null ? error.getTitle() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public String info() {
|
||||||
|
return error != null ? error.getDetails() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean badToken() {
|
||||||
|
return error != null && error.badToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Warning {
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String warnings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gson POJO for a MediaWiki API error.
|
||||||
|
*/
|
||||||
|
public class MwServiceError implements ServiceError {
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private String code;
|
||||||
|
@SuppressWarnings("unused") @Nullable private String info;
|
||||||
|
@SuppressWarnings("unused") @Nullable private String docref;
|
||||||
|
@SuppressWarnings("unused") @NonNull
|
||||||
|
private List<Message> messages = Collections.emptyList();
|
||||||
|
|
||||||
|
@Override @Nullable public String getTitle() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override @Nullable public String getDetails() {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public String getDocRef() {
|
||||||
|
return docref;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean badToken() {
|
||||||
|
return "badtoken".equals(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMessageName(@NonNull String messageName) {
|
||||||
|
for (Message msg : messages) {
|
||||||
|
if (messageName.equals(msg.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable public String getMessageHtml(@NonNull String messageName) {
|
||||||
|
for (Message msg : messages) {
|
||||||
|
if (messageName.equals(msg.name)) {
|
||||||
|
return msg.html();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toString() {
|
||||||
|
return "MwServiceError{"
|
||||||
|
+ "code='" + code + '\''
|
||||||
|
+ ", info='" + info + '\''
|
||||||
|
+ ", docref='" + docref + '\''
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Message {
|
||||||
|
@SuppressWarnings("unused") @Nullable private String name;
|
||||||
|
@SuppressWarnings("unused") @Nullable private String html;
|
||||||
|
|
||||||
|
@NonNull private String html() {
|
||||||
|
return StringUtils.defaultString(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package fr.free.nrw.commons.mwapi;
|
||||||
|
|
||||||
|
public interface ServiceError {
|
||||||
|
String getTitle();
|
||||||
|
|
||||||
|
String getDetails();
|
||||||
|
}
|
||||||
31
app/src/main/java/fr/free/nrw/commons/network/GsonUtil.java
Normal file
31
app/src/main/java/fr/free/nrw/commons/network/GsonUtil.java
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package fr.free.nrw.commons.network;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.json.UriTypeAdapter;
|
||||||
|
|
||||||
|
public final class GsonUtil {
|
||||||
|
private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss";
|
||||||
|
|
||||||
|
private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder()
|
||||||
|
.setDateFormat(DATE_FORMAT)
|
||||||
|
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe());
|
||||||
|
|
||||||
|
private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create();
|
||||||
|
|
||||||
|
public static Gson getDefaultGson() {
|
||||||
|
return DEFAULT_GSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static GsonBuilder getDefaultGsonBuilder() {
|
||||||
|
return DEFAULT_GSON_BUILDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GsonUtil() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.network;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import retrofit2.Retrofit;
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory;
|
||||||
|
|
||||||
|
public final class RetrofitFactory {
|
||||||
|
|
||||||
|
public static Retrofit newInstance(@NonNull String endpoint) {
|
||||||
|
return new Retrofit.Builder()
|
||||||
|
.client(new OkHttpClient())
|
||||||
|
.baseUrl(endpoint)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RetrofitFactory() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
public class MarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private String result;
|
||||||
|
|
||||||
|
public String result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QueryMarkReadResponse {
|
||||||
|
@SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,20 +7,15 @@ package fr.free.nrw.commons.notification;
|
||||||
public class Notification {
|
public class Notification {
|
||||||
public NotificationType notificationType;
|
public NotificationType notificationType;
|
||||||
public String notificationText;
|
public String notificationText;
|
||||||
|
public String date;
|
||||||
|
public String description;
|
||||||
|
public String link;
|
||||||
|
|
||||||
|
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) {
|
||||||
Notification (NotificationType notificationType, String notificationText) {
|
|
||||||
this.notificationType = notificationType;
|
this.notificationType = notificationType;
|
||||||
this.notificationText = notificationText;
|
this.notificationText = notificationText;
|
||||||
}
|
this.date = date;
|
||||||
|
this.description = description;
|
||||||
|
this.link = link;
|
||||||
public enum NotificationType {
|
|
||||||
/* Added for test purposes, needs to be rescheduled after implementing
|
|
||||||
fetching notifications from server */
|
|
||||||
edit,
|
|
||||||
mention,
|
|
||||||
message,
|
|
||||||
block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,27 @@
|
||||||
package fr.free.nrw.commons.notification;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import butterknife.Optional;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 18.12.2017.
|
* Created by root on 18.12.2017.
|
||||||
|
|
@ -18,9 +30,10 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||||
public class NotificationActivity extends NavigationBaseActivity {
|
public class NotificationActivity extends NavigationBaseActivity {
|
||||||
NotificationAdapterFactory notificationAdapterFactory;
|
NotificationAdapterFactory notificationAdapterFactory;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@BindView(R.id.listView) RecyclerView recyclerView;
|
@BindView(R.id.listView) RecyclerView recyclerView;
|
||||||
|
|
||||||
|
@Inject NotificationController controller;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -28,23 +41,48 @@ public class NotificationActivity extends NavigationBaseActivity {
|
||||||
setContentView(R.layout.activity_notification);
|
setContentView(R.layout.activity_notification);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
initListView();
|
initListView();
|
||||||
addNotifications();
|
|
||||||
initDrawer();
|
initDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initListView() {
|
private void initListView() {
|
||||||
recyclerView = findViewById(R.id.listView);
|
recyclerView = findViewById(R.id.listView);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
notificationAdapterFactory = new NotificationAdapterFactory(new NotificationRenderer.NotificationClicked() {
|
addNotifications();
|
||||||
@Override
|
|
||||||
public void notificationClicked(Notification notification) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
private void addNotifications() {
|
private void addNotifications() {
|
||||||
|
Timber.d("Add notifications");
|
||||||
|
|
||||||
recyclerView.setAdapter(notificationAdapterFactory.create(NotificationController.loadNotifications()));
|
Observable.fromCallable(() -> controller.getNotifications())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(notificationList -> {
|
||||||
|
Timber.d("Number of notifications is %d", notificationList.size());
|
||||||
|
setAdapter(notificationList);
|
||||||
|
}, throwable -> {
|
||||||
|
Timber.e(throwable, "Error occurred while loading notifications");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUrl(String url) {
|
||||||
|
if (url == null || url.equals("")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAdapter(List<Notification> notificationList) {
|
||||||
|
notificationAdapterFactory = new NotificationAdapterFactory(notification -> {
|
||||||
|
Timber.d("Notification clicked %s", notification.link);
|
||||||
|
handleUrl(notification.link);
|
||||||
|
});
|
||||||
|
RVRendererAdapter<Notification> adapter = notificationAdapterFactory.create(notificationList);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startYourself(Context context) {
|
||||||
|
Intent intent = new Intent(context, NotificationActivity.class);
|
||||||
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MwQueryResponse;
|
||||||
|
import fr.free.nrw.commons.network.RetrofitFactory;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Response;
|
||||||
|
import retrofit2.http.Field;
|
||||||
|
import retrofit2.http.FormUrlEncoded;
|
||||||
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.POST;
|
||||||
|
import retrofit2.http.Query;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public final class NotificationClient {
|
||||||
|
@NonNull
|
||||||
|
private final Service service;
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void success(@NonNull List<Notification> notifications);
|
||||||
|
|
||||||
|
void failure(Throwable t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationClient(@NonNull String endpoint) {
|
||||||
|
service = RetrofitFactory.newInstance(endpoint).create(Service.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static class CallbackAdapter implements retrofit2.Callback<MwQueryResponse<NotificationObject.QueryNotifications>> {
|
||||||
|
@NonNull
|
||||||
|
private final Callback callback;
|
||||||
|
|
||||||
|
CallbackAdapter(@NonNull Callback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<MwQueryResponse<NotificationObject.QueryNotifications>> call,
|
||||||
|
Response<MwQueryResponse<NotificationObject.QueryNotifications>> response) {
|
||||||
|
Timber.d("Resonse is %s", response);
|
||||||
|
if (response.body() != null && response.body().query() != null) {
|
||||||
|
callback.success(response.body().query().get());
|
||||||
|
} else {
|
||||||
|
callback.failure(new JsonParseException("Notification response is malformed."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<MwQueryResponse<NotificationObject.QueryNotifications>> call, Throwable caught) {
|
||||||
|
Timber.e(caught, "Error occurred while fetching notifications");
|
||||||
|
callback.failure(caught);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obrain a list of unread notifications for the user who is currently logged in.
|
||||||
|
*
|
||||||
|
* @param callback Callback that will receive the list of notifications.
|
||||||
|
* @param wikis List of wiki names for which notifications should be received. These must be
|
||||||
|
* in the "DB name" format, as in "enwiki", "zhwiki", "wikidatawiki", etc.
|
||||||
|
*/
|
||||||
|
public void getNotifications(@NonNull final Callback callback, @NonNull String... wikis) {
|
||||||
|
String wikiList = TextUtils.join("|", wikis);
|
||||||
|
requestNotifications(service, wikiList).enqueue(new CallbackAdapter(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@NonNull
|
||||||
|
Call<MwQueryResponse<NotificationObject.QueryNotifications>> requestNotifications(@NonNull Service service, @NonNull String wikiList) {
|
||||||
|
return service.getNotifications(wikiList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@NonNull
|
||||||
|
Call<MwQueryResponse<MarkReadResponse.QueryMarkReadResponse>> requestMarkRead(@NonNull Service service, @NonNull String token, @NonNull String idList) {
|
||||||
|
return service.markRead(token, idList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
interface Service {
|
||||||
|
String ACTION = "w/api.php?format=json&formatversion=2&action=";
|
||||||
|
|
||||||
|
@GET(ACTION + "query&meta=notifications¬filter=!read¬prop=list")
|
||||||
|
@NonNull
|
||||||
|
Call<MwQueryResponse<NotificationObject.QueryNotifications>> getNotifications(@Query("notwikis") @NonNull String wikiList);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(ACTION + "echomarkread")
|
||||||
|
@NonNull
|
||||||
|
Call<MwQueryResponse<MarkReadResponse.QueryMarkReadResponse>> markRead(@Field("token") @NonNull String token,
|
||||||
|
@Field("list") @NonNull String idList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
package fr.free.nrw.commons.notification;
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 19.12.2017.
|
* Created by root on 19.12.2017.
|
||||||
*/
|
*/
|
||||||
|
@Singleton
|
||||||
public class NotificationController {
|
public class NotificationController {
|
||||||
|
|
||||||
public static List<Notification> loadNotifications() {
|
private MediaWikiApi mediaWikiApi;
|
||||||
List<Notification> notifications = new ArrayList<>();
|
private SessionManager sessionManager;
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 1"));
|
|
||||||
notifications.add(new Notification(Notification.NotificationType.edit, "notification 2"));
|
@Inject
|
||||||
notifications.add(new Notification(Notification.NotificationType.mention, "notification 3"));
|
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 4"));
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
notifications.add(new Notification(Notification.NotificationType.edit, "notification 5"));
|
this.sessionManager = sessionManager;
|
||||||
notifications.add(new Notification(Notification.NotificationType.mention, "notification 6"));
|
}
|
||||||
notifications.add(new Notification(Notification.NotificationType.message, "notification 7"));
|
|
||||||
return notifications;
|
public List<Notification> getNotifications() throws IOException {
|
||||||
|
if (mediaWikiApi.validateLogin()) {
|
||||||
|
return mediaWikiApi.getNotifications();
|
||||||
|
} else {
|
||||||
|
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
|
||||||
|
if (authTokenValidated != null && authTokenValidated) {
|
||||||
|
return mediaWikiApi.getNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class NotificationObject {
|
||||||
|
public static final String TYPE_EDIT_USER_TALK = "edit-user-talk";
|
||||||
|
public static final String TYPE_REVERTED = "reverted";
|
||||||
|
public static final String TYPE_EDIT_THANK = "edit-thank";
|
||||||
|
public static final String WIKIDATA_WIKI = "wikidatawiki";
|
||||||
|
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull
|
||||||
|
private String wiki;
|
||||||
|
@SuppressWarnings("unused") private int id;
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String type;
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String category;
|
||||||
|
@SuppressWarnings("unused") private int revid;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private Title title;
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private Agent agent;
|
||||||
|
|
||||||
|
@NonNull public String wiki() {
|
||||||
|
return wiki;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public String type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public Agent agent() {
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public Title title() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int revID() {
|
||||||
|
return revid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFromWikidata() {
|
||||||
|
return wiki.equals(WIKIDATA_WIKI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toString() {
|
||||||
|
return Integer.toString(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Title {
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String full;
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String text;
|
||||||
|
@SuppressWarnings("unused") @Nullable
|
||||||
|
private String namespace;
|
||||||
|
@SuppressWarnings("unused") @SerializedName("namespace-key") private int namespaceKey;
|
||||||
|
|
||||||
|
@NonNull public String text() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public String full() {
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMainNamespace() {
|
||||||
|
return namespaceKey == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFull(@NonNull String title) {
|
||||||
|
full = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Agent {
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String id;
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private String name;
|
||||||
|
|
||||||
|
@NonNull public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NotificationList {
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private List<Notification> list;
|
||||||
|
|
||||||
|
@NonNull public List<Notification> getNotifications() {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QueryNotifications {
|
||||||
|
@SuppressWarnings("unused,NullableProblems") @NonNull private NotificationList notifications;
|
||||||
|
|
||||||
|
@NonNull public List<Notification> get() {
|
||||||
|
return notifications.getNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,13 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.pedrogomez.renderers.Renderer;
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.utils.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 19.12.2017.
|
* Created by root on 19.12.2017.
|
||||||
|
|
@ -45,23 +49,17 @@ public class NotificationRenderer extends Renderer<Notification> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
Notification notification = getContent();
|
Notification notification = getContent();
|
||||||
title.setText(notification.notificationText);
|
title.setText(notification.notificationText);
|
||||||
time.setText("3d");
|
time.setText(notification.date);
|
||||||
description.setText("Example notification description");
|
description.setText(notification.description);
|
||||||
switch (notification.notificationType) {
|
switch (notification.notificationType) {
|
||||||
case edit:
|
case THANK_YOU_EDIT:
|
||||||
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
icon.setImageResource(R.drawable.ic_edit_black_24dp);
|
||||||
break;
|
break;
|
||||||
case message:
|
default:
|
||||||
icon.setImageResource(R.drawable.ic_message_black_24dp);
|
icon.setImageResource(R.drawable.round_icon_unknown);
|
||||||
break;
|
}
|
||||||
case mention:
|
|
||||||
icon.setImageResource(R.drawable.ic_chat_bubble_black_24px);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon.setImageResource(R.drawable.round_icon_unknown);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface NotificationClicked{
|
public interface NotificationClicked{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
public enum NotificationType {
|
||||||
|
THANK_YOU_EDIT("thank-you-edit"),
|
||||||
|
EDIT_USER_TALK("edit-user-talk"),
|
||||||
|
MENTION("mention"),
|
||||||
|
WELCOME("welcome"),
|
||||||
|
UNKNOWN("unknown");
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
NotificationType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NotificationType handledValueOf(String name) {
|
||||||
|
for (NotificationType e : values()) {
|
||||||
|
if (e.getType().equals(name)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package fr.free.nrw.commons.notification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
|
public class NotificationUtils {
|
||||||
|
|
||||||
|
private static final String COMMONS_WIKI = "commonswiki";
|
||||||
|
|
||||||
|
public static boolean isCommonsNotification(Node document) {
|
||||||
|
if (document == null || !document.hasAttributes()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Element element = (Element) document;
|
||||||
|
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NotificationType getNotificationType(Node document) {
|
||||||
|
Element element = (Element) document;
|
||||||
|
String type = element.getAttribute("type");
|
||||||
|
return NotificationType.handledValueOf(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Notification getNotificationFromApiResult(Context context, Node document) {
|
||||||
|
NotificationType type = getNotificationType(document);
|
||||||
|
|
||||||
|
String notificationText = "";
|
||||||
|
String link = getNotificationLink(document);
|
||||||
|
String description = getNotificationDescription(document);
|
||||||
|
switch (type) {
|
||||||
|
case THANK_YOU_EDIT:
|
||||||
|
notificationText = context.getString(R.string.notifications_thank_you_edit);
|
||||||
|
break;
|
||||||
|
case EDIT_USER_TALK:
|
||||||
|
notificationText = getUserTalkMessage(context, document);
|
||||||
|
break;
|
||||||
|
case MENTION:
|
||||||
|
notificationText = getMentionMessage(context, document);
|
||||||
|
break;
|
||||||
|
case WELCOME:
|
||||||
|
notificationText = getWelcomeMessage(context, document);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return new Notification(type, notificationText, getTimestamp(document), description, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMentionMessage(Context context, Node document) {
|
||||||
|
String format = context.getString(R.string.notifications_mention);
|
||||||
|
return String.format(format, getAgent(document), getNotificationDescription(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getUserTalkMessage(Context context, Node document) {
|
||||||
|
String format = context.getString(R.string.notifications_talk_page_message);
|
||||||
|
return String.format(format, getAgent(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getWelcomeMessage(Context context, Node document) {
|
||||||
|
String welcomeMessageFormat = context.getString(R.string.notifications_welcome);
|
||||||
|
return String.format(welcomeMessageFormat, getAgent(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAgent(Node document) {
|
||||||
|
Element agentElement = (Element) getNode(document, "agent");
|
||||||
|
if (agentElement != null) {
|
||||||
|
return agentElement.getAttribute("name");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTimestamp(Node document) {
|
||||||
|
Element timestampElement = (Element) getNode(document, "timestamp");
|
||||||
|
if (timestampElement != null) {
|
||||||
|
return timestampElement.getAttribute("date");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getNotificationLink(Node document) {
|
||||||
|
String format = "%s%s";
|
||||||
|
Element titleElement = (Element) getNode(document, "title");
|
||||||
|
if (titleElement != null) {
|
||||||
|
String fullName = titleElement.getAttribute("full");
|
||||||
|
return String.format(format, BuildConfig.HOME_URL, fullName);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getNotificationDescription(Node document) {
|
||||||
|
Element titleElement = (Element) getNode(document, "title");
|
||||||
|
if (titleElement != null) {
|
||||||
|
return titleElement.getAttribute("text");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Node getNode(Node node, String nodeName) {
|
||||||
|
NodeList childNodes = node.getChildNodes();
|
||||||
|
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||||
|
Node nodeItem = childNodes.item(i);
|
||||||
|
Element item = (Element) nodeItem;
|
||||||
|
if (item.getTagName().equals(nodeName)) {
|
||||||
|
return nodeItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -146,7 +146,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_notifications:
|
case R.id.action_notifications:
|
||||||
drawerLayout.closeDrawer(navigationView);
|
drawerLayout.closeDrawer(navigationView);
|
||||||
startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
NotificationActivity.startYourself(this);
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
|
||||||
|
|
|
||||||
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
36
app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class DateUtils {
|
||||||
|
public static String getTimeAgo(Date currDate, Date itemDate) {
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
c.setTime(currDate);
|
||||||
|
int yearNow = c.get(Calendar.YEAR);
|
||||||
|
int monthNow = c.get(Calendar.MONTH);
|
||||||
|
int dayNow = c.get(Calendar.DAY_OF_MONTH);
|
||||||
|
int hourNow = c.get(Calendar.HOUR_OF_DAY);
|
||||||
|
int minuteNow = c.get(Calendar.MINUTE);
|
||||||
|
c.setTime(itemDate);
|
||||||
|
int videoYear = c.get(Calendar.YEAR);
|
||||||
|
int videoMonth = c.get(Calendar.MONTH);
|
||||||
|
int videoDays = c.get(Calendar.DAY_OF_MONTH);
|
||||||
|
int videoHour = c.get(Calendar.HOUR_OF_DAY);
|
||||||
|
int videoMinute = c.get(Calendar.MINUTE);
|
||||||
|
|
||||||
|
if (yearNow != videoYear) {
|
||||||
|
return (String.valueOf(yearNow - videoYear) + "-" + "years");
|
||||||
|
} else if (monthNow != videoMonth) {
|
||||||
|
return (String.valueOf(monthNow - videoMonth) + "-" + "months");
|
||||||
|
} else if (dayNow != videoDays) {
|
||||||
|
return (String.valueOf(dayNow - videoDays) + "-" + "days");
|
||||||
|
} else if (hourNow != videoHour) {
|
||||||
|
return (String.valueOf(hourNow - videoHour) + "-" + "hours");
|
||||||
|
} else if (minuteNow != videoMinute) {
|
||||||
|
return (String.valueOf(minuteNow - videoMinute) + "-" + "minutes");
|
||||||
|
} else {
|
||||||
|
return "0-seconds";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -214,4 +214,9 @@
|
||||||
<string name="location_permission_rationale_nearby">Permission required to display a list of nearby places</string>
|
<string name="location_permission_rationale_nearby">Permission required to display a list of nearby places</string>
|
||||||
<string name="get_directions">GET DIRECTIONS</string>
|
<string name="get_directions">GET DIRECTIONS</string>
|
||||||
<string name="read_article">READ ARTICLE</string>
|
<string name="read_article">READ ARTICLE</string>
|
||||||
|
|
||||||
|
<string name="notifications_welcome" formatted="false">Welcome to Wikimedia Commons, %s! We\'re glad you\'re here.</string>
|
||||||
|
<string name="notifications_talk_page_message" formatted="false">%s left a message on your talk page</string>
|
||||||
|
<string name="notifications_thank_you_edit">Thank you for making an edit</string>
|
||||||
|
<string name="notifications_mention" formatted="false">%s mentioned you on %s.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ public class TestCommonsApplication extends CommonsApplication {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaWikiApi provideMediaWikiApi() {
|
public MediaWikiApi provideMediaWikiApi(Context context) {
|
||||||
return mediaWikiApi;
|
return mediaWikiApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ android.useDeprecatedNdk=true
|
||||||
BUTTERKNIFE_VERSION=8.6.0
|
BUTTERKNIFE_VERSION=8.6.0
|
||||||
DAGGER_VERSION=2.13
|
DAGGER_VERSION=2.13
|
||||||
LEAK_CANARY=1.5.4
|
LEAK_CANARY=1.5.4
|
||||||
|
RETROFIT_VERSION=2.3.0
|
||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
|
||||||
#TODO: Temporary disabled. https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#aapt2
|
#TODO: Temporary disabled. https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#aapt2
|
||||||
|
|
|
||||||
BIN
notification_commons_clicked.png
Normal file
BIN
notification_commons_clicked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
notifications_commons.png
Normal file
BIN
notifications_commons.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Loading…
Add table
Add a link
Reference in a new issue