Commit 4fc1150b authored by Yusuke Iwaki's avatar Yusuke Iwaki

FIX #85 Merge branch 'reactive_notification' into develop

parents bb7371a5 f0b03146
......@@ -31,6 +31,8 @@
android:windowSoftInputMode="adjustResize"/>
<service android:name=".service.RocketChatService"/>
<service android:name=".service.notification.NotificationDismissalCallbackService"/>
</application>
</manifest>
......@@ -23,6 +23,8 @@ public class RoomSubscription extends RealmObject {
private boolean open;
private boolean alert;
private int unread;
private long _updatedAt;
private long ls; //last seen.
public String get_id() {
return _id;
......@@ -80,7 +82,35 @@ public class RoomSubscription extends RealmObject {
this.unread = unread;
}
public long get_updatedAt() {
return _updatedAt;
}
public void set_updatedAt(long _updatedAt) {
this._updatedAt = _updatedAt;
}
public long getLs() {
return ls;
}
public void setLs(long ls) {
this.ls = ls;
}
public static JSONObject customizeJson(JSONObject roomSubscriptionJson) throws JSONException {
if (!roomSubscriptionJson.isNull("ls")) {
long ls = roomSubscriptionJson.getJSONObject("ls").getLong("$date");
roomSubscriptionJson.remove("ls");
roomSubscriptionJson.put("ls", ls);
}
if (!roomSubscriptionJson.isNull("_updatedAt")) {
long updatedAt = roomSubscriptionJson.getJSONObject("_updatedAt").getLong("$date");
roomSubscriptionJson.remove("_updatedAt");
roomSubscriptionJson.put("_updatedAt", updatedAt);
}
return roomSubscriptionJson;
}
}
package chat.rocket.android.model.internal;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
/**
* View model for notification.
*/
public class NotificationItem extends RealmObject {
@PrimaryKey private String roomId;
private String title;
private String description;
private int unreadCount;
private String senderName;
private long contentUpdatedAt; //subscription._updatedAt
private long lastSeenAt; //max(notification dismissed, subscription.ls)
public String getRoomId() {
return roomId;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getUnreadCount() {
return unreadCount;
}
public void setUnreadCount(int unreadCount) {
this.unreadCount = unreadCount;
}
public String getSenderName() {
return senderName;
}
public void setSenderName(String senderName) {
this.senderName = senderName;
}
public long getContentUpdatedAt() {
return contentUpdatedAt;
}
public void setContentUpdatedAt(long contentUpdatedAt) {
this.contentUpdatedAt = contentUpdatedAt;
}
public long getLastSeenAt() {
return lastSeenAt;
}
public void setLastSeenAt(long lastSeenAt) {
this.lastSeenAt = lastSeenAt;
}
}
package chat.rocket.android.notification;
/**
* notifier.
*/
public interface Notifier {
void publishNotificationIfNeeded();
}
......@@ -22,6 +22,8 @@ import chat.rocket.android.service.observer.GetUsersOfRoomsProcedureObserver;
import chat.rocket.android.service.observer.LoadMessageProcedureObserver;
import chat.rocket.android.service.observer.MethodCallObserver;
import chat.rocket.android.service.observer.NewMessageObserver;
import chat.rocket.android.service.observer.NotificationItemObserver;
import chat.rocket.android.service.observer.ReactiveNotificationManager;
import chat.rocket.android.service.observer.SessionObserver;
import chat.rocket.android.service.observer.TokenLoginObserver;
import chat.rocket.android_ddp.DDPClientCallback;
......@@ -45,7 +47,9 @@ public class RocketChatWebSocketThread extends HandlerThread {
LoadMessageProcedureObserver.class,
GetUsersOfRoomsProcedureObserver.class,
NewMessageObserver.class,
CurrentUserObserver.class
CurrentUserObserver.class,
ReactiveNotificationManager.class,
NotificationItemObserver.class
};
private final Context appContext;
private final String serverConfigId;
......
package chat.rocket.android.service.ddp.stream;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.notification.Notifier;
import chat.rocket.android.notification.StreamNotifyUserNotifier;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class StreamNotifyUserNotification extends AbstractStreamNotifyUserEventSubscriber {
public StreamNotifyUserNotification(Context context, String hostname, RealmHelper realmHelper,
DDPClientWraper ddpClient, String userId) {
super(context, hostname, realmHelper, ddpClient, userId);
}
@Override protected String getSubscriptionSubParam() {
return "notification";
}
@Override protected void handleArgs(JSONArray args) throws JSONException {
JSONObject target = args.getJSONObject(args.length() - 1);
Notifier notifier = new StreamNotifyUserNotifier(context, hostname,
target.getString("title"),
target.getString("text"),
target.getJSONObject("payload"));
notifier.publishNotificationIfNeeded();
}
@Override protected Class<? extends RealmObject> getModelClass() {
// not used because handleArgs is override.
return null;
}
@Override protected String getPrimaryKeyForModel() {
// not used because handleArgs is override.
return null;
}
}
......@@ -5,6 +5,8 @@ import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
import org.json.JSONException;
import org.json.JSONObject;
public class StreamNotifyUserSubscriptionsChanged extends AbstractStreamNotifyUserEventSubscriber {
public StreamNotifyUserSubscriptionsChanged(Context context, String hostname,
......@@ -20,6 +22,10 @@ public class StreamNotifyUserSubscriptionsChanged extends AbstractStreamNotifyUs
return RoomSubscription.class;
}
@Override protected JSONObject customizeFieldJson(JSONObject json) throws JSONException {
return RoomSubscription.customizeJson(super.customizeFieldJson(json));
}
@Override protected String getPrimaryKeyForModel() {
return "rid";
}
......
package chat.rocket.android.service.notification;
import android.app.IntentService;
import android.content.Intent;
import chat.rocket.android.model.internal.NotificationItem;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmStore;
/**
* triggered when notification is dismissed.
*/
public class NotificationDismissalCallbackService extends IntentService {
public NotificationDismissalCallbackService() {
super(NotificationDismissalCallbackService.class.getSimpleName());
}
@Override protected void onHandleIntent(Intent intent) {
if (!intent.hasExtra("serverConfigId") || !intent.hasExtra("roomId")) {
return;
}
String serverConfigId = intent.getStringExtra("serverConfigId");
String roomId = intent.getStringExtra("roomId");
RealmHelper realmHelper = RealmStore.get(serverConfigId);
if (realmHelper == null) {
return;
}
realmHelper.executeTransaction(realm -> {
NotificationItem item =
realm.where(NotificationItem.class).equalTo("roomId", roomId).findFirst();
if (item != null) {
long currentTime = System.currentTimeMillis();
if (item.getLastSeenAt() <= currentTime) {
item.setLastSeenAt(currentTime);
}
}
return null;
});
}
}
......@@ -3,10 +3,10 @@ package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.ddp.User;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.Registerable;
import chat.rocket.android.service.ddp.stream.StreamNotifyUserNotification;
import chat.rocket.android.service.ddp.stream.StreamNotifyUserSubscriptionsChanged;
import hugo.weaving.DebugLog;
import io.realm.Realm;
......@@ -66,12 +66,7 @@ public class CurrentUserObserver extends AbstractModelObserver<User> {
listeners.add(listener);
}
return null;
});
Registerable listener = new StreamNotifyUserNotification(
context, hostname, realmHelper, ddpClient, userId);
listener.register();
listeners.add(listener);
}).continueWith(new LogcatIfError());
}
@DebugLog
......
package chat.rocket.android.notification;
package chat.rocket.android.service.observer;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import bolts.Task;
import chat.rocket.android.R;
import chat.rocket.android.activity.MainActivity;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.helper.Avatar;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.internal.NotificationItem;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmStore;
import org.json.JSONException;
import org.json.JSONObject;
import chat.rocket.android.service.notification.NotificationDismissalCallbackService;
import io.realm.Realm;
import io.realm.RealmResults;
import java.util.List;
/**
* utility class for notification.
* observes NotificationItem and notify/cancel notification.
*/
public class StreamNotifyUserNotifier implements Notifier {
private final Context context;
private final String hostname;
private final String title;
private final String text;
private final JSONObject payload;
public StreamNotifyUserNotifier(Context context, String hostname,
String title, String text, JSONObject payload) {
this.context = context;
this.hostname = hostname;
this.title = title;
this.text = text;
this.payload = payload;
public class NotificationItemObserver extends AbstractModelObserver<NotificationItem> {
public NotificationItemObserver(Context context, String hostname, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, hostname, realmHelper, ddpClient);
}
@Override public void publishNotificationIfNeeded() {
if (!shouldNotify()) {
@Override public RealmResults<NotificationItem> queryItems(Realm realm) {
return realm.where(NotificationItem.class).findAll();
}
@Override public void onUpdateResults(List<NotificationItem> results) {
if (results.isEmpty()) {
return;
}
generateNotificationAsync().onSuccess(task -> {
NotificationManagerCompat.from(context)
.notify(generateNotificationId(), task.getResult());
return null;
});
}
for (NotificationItem item : results) {
final String notificationId = item.getRoomId();
private boolean shouldNotify() {
// TODO: should check if target message is already read or not.
return true;
if (item.getUnreadCount() > 0
&& item.getContentUpdatedAt() > item.getLastSeenAt()) {
generateNotificationFor(item)
.onSuccess(task -> {
Notification notification = task.getResult();
if (notification != null) {
NotificationManagerCompat.from(context)
.notify(notificationId.hashCode(), notification);
}
return null;
});
} else {
NotificationManagerCompat.from(context).cancel(notificationId.hashCode());
}
}
}
private int generateNotificationId() {
// TODO: should summary notification by user or room.
return (int) (System.currentTimeMillis() % Integer.MAX_VALUE);
}
private Task<Notification> generateNotificationFor(NotificationItem item) {
final String username = item.getSenderName();
final String roomId = item.getRoomId();
final String title = item.getTitle();
final String description = TextUtils.or(item.getDescription(), "").toString();
final int unreadCount = item.getUnreadCount();
if (TextUtils.isEmpty(username)) {
return Task.forResult(generateNotification(roomId, title, description, unreadCount, null));
}
private Task<Notification> generateNotificationAsync() {
int size = context.getResources().getDimensionPixelSize(R.dimen.notification_avatar_size);
return getUsername()
.onSuccessTask(task -> new Avatar(hostname, task.getResult()).getBitmap(context, size))
return new Avatar(hostname, username).getBitmap(context, size)
.continueWithTask(task -> {
Bitmap icon = task.isFaulted() ? null : task.getResult();
return Task.forResult(generateNotification(icon));
final Notification notification =
generateNotification(roomId, title, description, unreadCount, icon);
return Task.forResult(notification);
});
}
private Task<String> getUsername() {
try {
return Task.forResult(payload.getJSONObject("sender").getString("username"));
} catch (Exception exception) {
return Task.forError(exception);
private PendingIntent getContentIntent(String roomId) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_CLEAR_TOP);
ServerConfig config = RealmStore.getDefault().executeTransactionForRead(realm ->
realm.where(ServerConfig.class).equalTo("hostname", hostname).findFirst());
if (config != null) {
intent.putExtra("serverConfigId", config.getServerConfigId());
intent.putExtra("roomId", roomId);
}
return PendingIntent.getActivity(context.getApplicationContext(),
(int) (System.currentTimeMillis() % Integer.MAX_VALUE),
intent, PendingIntent.FLAG_ONE_SHOT);
}
private Notification generateNotification(Bitmap largeIcon) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_CLEAR_TOP);
private PendingIntent getDeleteIntent(String roomId) {
Intent intent = new Intent(context, NotificationDismissalCallbackService.class);
ServerConfig config = RealmStore.getDefault().executeTransactionForRead(realm ->
realm.where(ServerConfig.class).equalTo("hostname", hostname).findFirst());
realm.where(ServerConfig.class).equalTo("hostname", hostname).findFirst());
if (config != null) {
intent.putExtra("serverConfigId", config.getServerConfigId());
try {
intent.putExtra("roomId", payload.getString("rid"));
} catch (JSONException exception) {
}
intent.putExtra("roomId", roomId);
}
PendingIntent pendingIntent = PendingIntent.getActivity(context.getApplicationContext(),
return PendingIntent.getService(context.getApplicationContext(),
(int) (System.currentTimeMillis() % Integer.MAX_VALUE),
intent, PendingIntent.FLAG_ONE_SHOT);
}
private Notification generateNotification(String roomId, String title,
@NonNull String description, int unreadCount, @Nullable Bitmap icon) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setContentTitle(title)
.setContentText(text)
.setAutoCancel(true)
.setContentText(description)
.setNumber(unreadCount)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setSmallIcon(R.drawable.rocket_chat_notification_24dp)
.setContentIntent(pendingIntent);
if (largeIcon != null) {
builder.setLargeIcon(largeIcon);
.setContentIntent(getContentIntent(roomId))
.setDeleteIntent(getDeleteIntent(roomId));
if (icon != null) {
builder.setLargeIcon(icon);
}
if (text.length() > 20) {
if (description.length() > 20) {
return new NotificationCompat.BigTextStyle(builder)
.bigText(text)
.bigText(description)
.build();
} else {
return builder.build();
......
package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.model.internal.NotificationItem;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.Realm;
import io.realm.RealmResults;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* observing room subscriptions with unread>0.
*/
public class ReactiveNotificationManager extends AbstractModelObserver<RoomSubscription> {
public ReactiveNotificationManager(Context context, String hostname,
RealmHelper realmHelper, DDPClientWraper ddpClient) {
super(context, hostname, realmHelper, ddpClient);
}
@Override public RealmResults<RoomSubscription> queryItems(Realm realm) {
return realm.where(RoomSubscription.class)
.equalTo("open", true)
.findAll();
}
@Override public void onUpdateResults(List<RoomSubscription> roomSubscriptions) {
JSONArray notifications = new JSONArray();
for (RoomSubscription roomSubscription : roomSubscriptions) {
final String roomId = roomSubscription.getRid();
NotificationItem item = realmHelper.executeTransactionForRead(realm ->
realm.where(NotificationItem.class).equalTo("roomId", roomId).findFirst());
long lastSeenAt = Math.max(item != null ? item.getLastSeenAt() : 0, roomSubscription.getLs());
try {
JSONObject notification = new JSONObject()
.put("roomId", roomSubscription.getRid())
.put("title", roomSubscription.getName())
.put("description", "new message")
.put("unreadCount", roomSubscription.getUnread())
.put("contentUpdatedAt", roomSubscription.get_updatedAt())
.put("lastSeenAt", lastSeenAt);
if (RoomSubscription.TYPE_DIRECT_MESSAGE.equals(roomSubscription.getT())) {
notification.put("senderName", roomSubscription.getName());
} else {
notification.put("senderName", JSONObject.NULL);
}
notifications.put(notification);
} catch (JSONException exception) {
RCLog.w(exception);
}
}
realmHelper.executeTransaction(realm -> {
realm.createOrUpdateAllFromJson(NotificationItem.class, notifications);
return null;
}).continueWith(new LogcatIfError());
}
}
......@@ -76,8 +76,20 @@ public class RealmHelper {
}
}
private boolean shouldUseSync() {
// ref: realm-java:realm/realm-library/src/main/java/io/realm/AndroidNotifier.java
// #isAutoRefreshAvailable()
if (Looper.myLooper() == null) {
return true;
}
String threadName = Thread.currentThread().getName();
return threadName != null && threadName.startsWith("IntentService[");
}
public Task<Void> executeTransaction(final RealmHelper.Transaction transaction) {
return Looper.myLooper() == null ? executeTransactionSync(transaction)
return shouldUseSync() ? executeTransactionSync(transaction)
: executeTransactionAsync(transaction);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment