Skip to content

Commit

Permalink
Enable crosswiki notifications and minor UI fixes in displaying notif… (
Browse files Browse the repository at this point in the history
commons-app#1540)

* Enable crosswiki notifications and minor UI fixes in displaying notifications

* Added java docs
  • Loading branch information
Vivek Maskara authored and neslihanturan committed May 24, 2018
1 parent 41acb76 commit 2a0b9d8
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 23 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ android:
components:
- tools
- platform-tools
- build-tools-26.0.2
- build-tools-27.0.0
- extra-google-m2repository
- extra-android-m2repository
- ${ANDROID_TARGET}
- android-25
- android-26
- android-27
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
licenses:
- 'android-sdk-license-.+'
Expand Down
6 changes: 5 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ dependencies {
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'

implementation 'com.caverock:androidsvg:1.2.1'
implementation 'com.github.bumptech.glide:glide:4.7.1'
kapt 'com.github.bumptech.glide:compiler:4.7.1'

androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
Expand Down Expand Up @@ -117,7 +121,7 @@ android {
buildTypes {
release {
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt'
}
debug {
applicationIdSuffix ".debug"
Expand Down
9 changes: 9 additions & 0 deletions app/proguard-glide.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
36 changes: 36 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package fr.free.nrw.commons.glide;

import android.support.annotation.NonNull;

import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;

import java.io.IOException;
import java.io.InputStream;

/**
* Decodes an SVG internal representation from an {@link InputStream}.
*/
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {

@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}

public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fr.free.nrw.commons.glide;

import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;

/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fr.free.nrw.commons.glide;

import android.graphics.drawable.PictureDrawable;
import android.widget.ImageView;

import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;

/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {

/**
* Sets the layer type to none if the load fails
* @param e
* @param model
* @param target
* @param isFirstResource
* @return
*/
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}

/**
* Sets the layer type to software when the resource is ready
* @param resource
* @param model
* @param target
* @param dataSource
* @param isFirstResource
* @return
*/
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ public List<Notification> getNotifications() {
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
// .param("meta", "notifications")
.param("notformat", "model")
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package fr.free.nrw.commons.notification;

import android.util.Log;
import android.graphics.drawable.PictureDrawable;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.bumptech.glide.RequestBuilder;
import com.pedrogomez.renderers.Renderer;

import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter;

import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;

/**
* Created by root on 19.12.2017.
*/

public class NotificationRenderer extends Renderer<Notification> {
private RequestBuilder<PictureDrawable> requestBuilder;

@BindView(R.id.title) ReadMoreTextView title;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
Expand All @@ -41,23 +48,32 @@ protected void hookListeners(View rootView) {
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView);
requestBuilder = GlideApp.with(inflatedView.getContext())
.as(PictureDrawable.class)
.error(R.drawable.round_icon_unknown)
.transition(withCrossFade())
.listener(new SvgSoftwareLayerSetter());
return inflatedView;
}

@Override
public void render() {
Notification notification = getContent();
String str = notification.notificationText.trim();
str = str.concat(" ");
title.setText(str);
setTitle(notification.notificationText);
time.setText(notification.date);
switch (notification.notificationType) {
case THANK_YOU_EDIT:
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break;
default:
icon.setImageResource(R.drawable.round_icon_unknown);
}
requestBuilder.load(notification.iconUrl).into(icon);
}

/**
* Cleans up the notification text and sets it as the title
* Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification
* @param notificationText
*/
private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" ");
title.setText(notificationText);
}

public interface NotificationClicked{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;

import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;

public class NotificationUtils {

private static final String COMMONS_WIKI = "commonswiki";
private static final String WIKIDATA_WIKI = "wikidatawiki";
private static final String WIKIPEDIA_WIKI = "enwiki";

public static boolean isCommonsNotification(Node document) {
if (document == null || !document.hasAttributes()) {
Expand All @@ -31,6 +32,32 @@ public static boolean isCommonsNotification(Node document) {
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
}

/**
* Returns true if the wiki attribute corresponds to wikidatawiki
* @param document
* @return
*/
public static boolean isWikidataNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIDATA_WIKI.equals(element.getAttribute("wiki"));
}

/**
* Returns true if the wiki attribute corresponds to enwiki
* @param document
* @return
*/
public static boolean isWikipediaNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki"));
}

public static NotificationType getNotificationType(Node document) {
Element element = (Element) document;
String type = element.getAttribute("type");
Expand Down Expand Up @@ -68,10 +95,17 @@ public static List<Notification> getNotificationsFromList(Context context, NodeL
return notifications;
}

/**
* Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia
* This function returns true only if the notification belongs to any of the above wikis and is of a known notification type
* @param node
* @return
*/
private static boolean isUsefulNotification(Node node) {
return isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)
&& !getNotificationType(node).equals(THANK_YOU_EDIT);
return (isCommonsNotification(node)
|| isWikidataNotification(node)
|| isWikipediaNotification(node))
&& !getNotificationType(node).equals(UNKNOWN);
}

public static boolean isBundledNotification(Node document) {
Expand All @@ -97,7 +131,7 @@ public static Notification getNotificationFromApiResult(Context context, Node do

switch (type) {
case THANK_YOU_EDIT:
notificationText = context.getString(R.string.notifications_thank_you_edit);
notificationText = getThankYouEditDescription(document);
break;
case EDIT_USER_TALK:
notificationText = getNotificationText(document);
Expand Down Expand Up @@ -146,6 +180,16 @@ private static String getMentionDescription(Node document) {
return body != null ? body.getTextContent() : "";
}

/**
* Gets the header node returned in the XML document to form the description for thank you edits
* @param document
* @return
*/
private static String getThankYouEditDescription(Node document) {
Node body = getNode(getModel(document), "header");
return body != null ? body.getTextContent() : "";
}

private static String getNotificationIconUrl(Node document) {
String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl");
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package fr.free.nrw.commons.notification;

import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;

import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;

import java.io.InputStream;

import fr.free.nrw.commons.glide.SvgDecoder;
import fr.free.nrw.commons.glide.SvgDrawableTranscoder;

/**
* Module for the SVG sample app.
*/
@GlideModule
public class SvgModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}

// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}
Loading

0 comments on commit 2a0b9d8

Please sign in to comment.