Unverified Commit d4f4c5b4 authored by Leonardo Aramaki's avatar Leonardo Aramaki Committed by GitHub

Merge pull request #541 from RocketChat/fix/notifications

[WIP] New rewritten push notification handler
parents 9613b461 0846097e
......@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<permission
android:name="chat.rocket.android.permission.C2D_MESSAGE"
......@@ -56,10 +57,32 @@
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE"/>
<category android:name="com.example.gcm"/>
<action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
<category android:name="chat.rocket.android"/>
</intent-filter>
</receiver>
<receiver
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE"/>
<category android:name="chat.rocket.android"/>
</intent-filter>
</receiver>
<receiver
android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver"
android:exported="false" />
<service android:name="com.google.firebase.iid.FirebaseInstanceIdService"
android:exported="true">
<intent-filter android:priority="-500">
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service
android:name=".push.gcm.GCMIntentService"
android:exported="false">
......@@ -76,6 +99,12 @@
</intent-filter>
</service>
<receiver android:name=".push.PushManager$DeleteReceiver"
android:exported="false" />
<receiver android:name=".push.PushManager$ReplyReceiver"
android:exported="false" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
......
......@@ -22,12 +22,15 @@ import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.annotations.NonNull;
import io.reactivex.annotations.Nullable;
import okhttp3.HttpUrl;
/**
* sharedpreference-based cache.
*/
public class RocketChatCache {
private static final String KEY_SELECTED_SERVER_HOSTNAME = "KEY_SELECTED_SERVER_HOSTNAME";
private static final String KEY_SELECTED_SITE_URL = "KEY_SELECTED_SITE_URL";
private static final String KEY_SELECTED_SITE_NAME = "KEY_SELECTED_SITE_NAME";
private static final String KEY_SELECTED_ROOM_ID = "KEY_SELECTED_ROOM_ID";
private static final String KEY_PUSH_ID = "KEY_PUSH_ID";
private static final String KEY_HOSTNAME_LIST = "KEY_HOSTNAME_LIST";
......@@ -50,6 +53,72 @@ public class RocketChatCache {
setString(KEY_SELECTED_SERVER_HOSTNAME, newHostname);
}
public void addHostSiteName(@NonNull String currentHostname, @NonNull String siteName) {
try {
String hostSiteNamesJson = getHostSiteNamesJson();
JSONObject jsonObject = (hostSiteNamesJson == null) ?
new JSONObject() : new JSONObject(hostSiteNamesJson);
jsonObject.put(currentHostname, siteName);
setString(KEY_SELECTED_SITE_NAME, jsonObject.toString());
} catch (JSONException e) {
RCLog.e(e);
}
}
public @NonNull String getHostSiteName(@NonNull String host) {
if (host.startsWith("http")) {
HttpUrl url = HttpUrl.parse(host);
if (url != null) {
host = url.host();
}
}
try {
String hostSiteNamesJson = getHostSiteNamesJson();
JSONObject jsonObject = (hostSiteNamesJson == null) ?
new JSONObject() : new JSONObject(hostSiteNamesJson);
host = getSiteUrlFor(host);
return jsonObject.optString(host);
} catch (JSONException e) {
RCLog.e(e);
}
return "";
}
private @Nullable String getHostSiteNamesJson() {
return getString(KEY_SELECTED_SITE_NAME, null);
}
public void addHostnameSiteUrl(@Nullable String hostnameAlias, @NonNull String currentHostname) {
String alias = null;
if (hostnameAlias != null) {
alias = hostnameAlias.toLowerCase();
}
try {
String selectedHostnameAliasJson = getLoginHostnamesJson();
JSONObject jsonObject = selectedHostnameAliasJson == null ?
new JSONObject() : new JSONObject(selectedHostnameAliasJson);
jsonObject.put(alias, currentHostname);
setString(KEY_SELECTED_SITE_URL, jsonObject.toString());
} catch (JSONException e) {
RCLog.e(e);
}
}
public @NonNull String getSiteUrlFor(String hostname) {
try {
String selectedServerHostname = getSelectedServerHostname();
return new JSONObject(getLoginHostnamesJson())
.optString(hostname, selectedServerHostname);
} catch (JSONException e) {
RCLog.e(e);
}
return null;
}
private @Nullable String getLoginHostnamesJson() {
return getString(KEY_SELECTED_SITE_URL, null);
}
public void addHostname(@NonNull String hostname, @Nullable String hostnameAvatarUri, String siteName) {
String hostnameList = getString(KEY_HOSTNAME_LIST, null);
try {
......
......@@ -3,21 +3,24 @@ package chat.rocket.android.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.hadisatrio.optional.Optional;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import chat.rocket.android.LaunchUtil;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.helper.Logger;
import chat.rocket.android.push.PushConstants;
import chat.rocket.android.push.PushNotificationHandler;
import chat.rocket.android.push.PushManager;
import chat.rocket.android.service.ConnectivityManager;
import chat.rocket.core.models.ServerInfo;
import chat.rocket.persistence.realm.RealmStore;
import chat.rocket.persistence.realm.models.ddp.RealmRoom;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.HttpUrl;
abstract class AbstractAuthedActivity extends AbstractFragmentActivity {
@State protected String hostname;
......@@ -53,20 +56,36 @@ abstract class AbstractAuthedActivity extends AbstractFragmentActivity {
return;
}
if (intent.hasExtra(PushConstants.HOSTNAME)) {
rocketChatCache.setSelectedServerHostname(intent.getStringExtra(PushConstants.HOSTNAME));
if (intent.hasExtra(PushConstants.ROOM_ID)) {
rocketChatCache.setSelectedRoomId(intent.getStringExtra(PushConstants.ROOM_ID));
if (intent.hasExtra(PushManager.EXTRA_HOSTNAME)) {
String hostname = intent.getStringExtra(PushManager.EXTRA_HOSTNAME);
HttpUrl url = HttpUrl.parse(hostname);
if (url != null) {
String hostnameFromPush = url.host();
String loginHostname = rocketChatCache.getSiteUrlFor(hostnameFromPush);
rocketChatCache.setSelectedServerHostname(loginHostname);
if (intent.hasExtra(PushManager.EXTRA_ROOM_ID)) {
rocketChatCache.setSelectedRoomId(intent.getStringExtra(PushManager.EXTRA_ROOM_ID));
}
}
PushManager.INSTANCE.clearNotificationsByHost(hostname);
} else {
updateHostnameIfNeeded(rocketChatCache.getSelectedServerHostname());
}
if (intent.hasExtra(PushConstants.NOT_ID)) {
if (intent.hasExtra(PushManager.EXTRA_NOT_ID) && intent.hasExtra(PushManager.EXTRA_HOSTNAME)) {
isNotification = true;
PushNotificationHandler
.cleanUpNotificationStack(intent.getIntExtra(PushConstants.NOT_ID, 0));
int notificationId = intent.getIntExtra(PushManager.EXTRA_NOT_ID, 0);
String hostname = intent.getStringExtra(PushManager.EXTRA_HOSTNAME);
HttpUrl url = HttpUrl.parse(hostname);
if (url != null) {
String hostnameFromPush = url.host();
String loginHostname = rocketChatCache.getSiteUrlFor(hostnameFromPush);
PushManager.INSTANCE.clearNotificationsByHostAndNotificationId(loginHostname, notificationId);
} else {
PushManager.INSTANCE.clearNotificationsByNotificationId(notificationId);
}
}
}
......
......@@ -74,8 +74,10 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
onHostnameUpdated();
}
} else {
presenter.bindViewOnly(this);
ConnectivityManager.getInstance(getApplicationContext()).keepAliveServer();
presenter.bindView(this);
presenter.loadSignedInServers(hostname);
roomId = new RocketChatCache(getApplicationContext()).getSelectedRoomId();
}
}
......@@ -172,7 +174,7 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
sessionInteractor,
new MethodCallHelper(this, hostname),
ConnectivityManager.getInstance(getApplicationContext()),
rocketChatCache,
rocketChatCache,
publicSettingRepository
);
......@@ -302,6 +304,15 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
}
}
@Override
public void refreshRoom() {
Fragment fragment = getSupportFragmentManager().findFragmentById(getLayoutContainerForFragment());
if (fragment != null && fragment instanceof RoomFragment) {
RoomFragment roomFragment = (RoomFragment) fragment;
roomFragment.loadMessages();
}
}
private void changeServerIfNeeded(String serverHostname) {
if (!hostname.equalsIgnoreCase(serverHostname)) {
RocketChatCache rocketChatCache = new RocketChatCache(getApplicationContext());
......@@ -328,7 +339,6 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
public static final int STATUS_DISMISS = 0;
public static final int STATUS_CONNECTION_ERROR = 1;
public static final int STATUS_TOKEN_LOGIN = 2;
public static final int STATUS_LOGGING_OUT = 3;
private int status;
private Snackbar snackbar;
......
......@@ -26,6 +26,8 @@ public interface MainContract {
void showConnectionOk();
void showSignedInServers(List<Pair<String, Pair<String, String>>> serverList);
void refreshRoom();
}
interface Presenter extends BaseContract.Presenter<View> {
......
......@@ -225,6 +225,7 @@ public class MainPresenter extends BasePresenter<MainContract.View>
connectivity -> {
if (connectivity.state == ServerConnectivity.STATE_CONNECTED) {
view.showConnectionOk();
view.refreshRoom();
return;
}
view.showConnecting();
......
......@@ -11,11 +11,13 @@ import java.util.UUID;
import bolts.Continuation;
import bolts.Task;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.service.ConnectivityManager;
import chat.rocket.android.service.DDPClientRef;
import chat.rocket.android_ddp.DDPClientCallback;
import chat.rocket.core.PublicSettingsConstants;
import chat.rocket.core.SyncState;
import chat.rocket.persistence.realm.RealmHelper;
import chat.rocket.persistence.realm.RealmStore;
......@@ -30,6 +32,7 @@ import chat.rocket.persistence.realm.models.ddp.RealmSpotlightUser;
import chat.rocket.persistence.realm.models.internal.MethodCall;
import chat.rocket.persistence.realm.models.internal.RealmSession;
import hugo.weaving.DebugLog;
import okhttp3.HttpUrl;
/**
* Utility class for creating/handling MethodCall or RPC.
......@@ -65,6 +68,12 @@ public class MethodCallHelper {
this.ddpClientRef = ddpClientRef;
}
public MethodCallHelper(Context context, RealmHelper realmHelper, DDPClientRef ddpClientRef) {
this.context = context.getApplicationContext();
this.realmHelper = realmHelper;
this.ddpClientRef = ddpClientRef;
}
@DebugLog
private Task<String> executeMethodCall(String methodName, String param, long timeout) {
if (ddpClientRef != null) {
......@@ -422,13 +431,31 @@ public class MethodCallHelper {
.onSuccessTask(task -> Task.forResult(null));
}
public Task<Void> getPublicSettings() {
public Task<Void> getPublicSettings(String currentHostname) {
return call("public-settings/get", TIMEOUT_MS)
.onSuccessTask(CONVERT_TO_JSON_ARRAY)
.onSuccessTask(task -> {
final JSONArray settings = task.getResult();
String siteUrl = null;
String siteName = null;
for (int i = 0; i < settings.length(); i++) {
RealmPublicSetting.customizeJson(settings.getJSONObject(i));
JSONObject jsonObject = settings.getJSONObject(i);
RealmPublicSetting.customizeJson(jsonObject);
if (isPublicSetting(jsonObject, PublicSettingsConstants.General.SITE_URL)) {
siteUrl = jsonObject.getString(RealmPublicSetting.VALUE);
} else if (isPublicSetting(jsonObject, PublicSettingsConstants.General.SITE_NAME)) {
siteName = jsonObject.getString(RealmPublicSetting.VALUE);
}
}
if (siteName != null && siteUrl != null) {
HttpUrl httpSiteUrl = HttpUrl.parse(siteUrl);
if (httpSiteUrl != null) {
String host = httpSiteUrl.host();
RocketChatCache rocketChatCache = new RocketChatCache(context);
rocketChatCache.addHostnameSiteUrl(host, currentHostname);
rocketChatCache.addHostSiteName(currentHostname, siteName);
}
}
return realmHelper.executeTransaction(realm -> {
......@@ -439,6 +466,10 @@ public class MethodCallHelper {
});
}
private boolean isPublicSetting(JSONObject jsonObject, String id) {
return jsonObject.optString(RealmPublicSetting.ID).equalsIgnoreCase(id);
}
public Task<Void> getPermissions() {
return call("permissions/get", TIMEOUT_MS)
.onSuccessTask(CONVERT_TO_JSON_ARRAY)
......
......@@ -715,4 +715,8 @@ public class RoomFragment extends AbstractChatRoomFragment implements
startActivity(intent);
}
}
public void loadMessages() {
presenter.loadMessages();
}
}
\ No newline at end of file
package chat.rocket.android.push
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.support.v4.app.RemoteInput
import android.text.Html
import android.text.Spanned
import android.util.Log
import chat.rocket.android.BackgroundLooper
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.RocketChatCache
import chat.rocket.android.activity.MainActivity
import chat.rocket.android.helper.Logger
import chat.rocket.core.interactors.MessageInteractor
import chat.rocket.core.models.Room
import chat.rocket.core.models.User
import chat.rocket.persistence.realm.repositories.RealmMessageRepository
import chat.rocket.persistence.realm.repositories.RealmRoomRepository
import chat.rocket.persistence.realm.repositories.RealmUserRepository
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import okhttp3.HttpUrl
import org.json.JSONObject
import java.io.Serializable
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.HashMap
typealias TupleRoomUser = Pair<Room, User>
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
object PushManager {
const val EXTRA_NOT_ID = "chat.rocket.android.EXTRA_NOT_ID"
const val EXTRA_HOSTNAME = "chat.rocket.android.EXTRA_HOSTNAME"
const val EXTRA_PUSH_MESSAGE = "chat.rocket.android.EXTRA_PUSH_MESSAGE"
const val EXTRA_ROOM_ID = "chat.rocket.android.EXTRA_ROOM_ID"
private const val REPLY_LABEL = "REPLY"
private const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY"
// Notifications received from the same server are grouped in a single bundled notification.
// This map associates a host to a group id.
private val groupMap = HashMap<String, TupleGroupIdMessageCount>()
// Map a hostname to a list of push messages that pertain to it.
private val hostToPushMessageList = HashMap<String, MutableList<PushMessage>>()
private val randomizer = Random()
/**
* Handles a receiving push by creating and displaying an appropriate notification based
* on the *data* param bundle received.
*/
@Synchronized
fun handle(context: Context, data: Bundle) {
val appContext = context.applicationContext
val message = data["message"] as String
val image = data["image"] as String
val ejson = data["ejson"] as String
val notId = data["notId"] as String
val style = data["style"] as String
val summaryText = data["summaryText"] as String
val count = data["count"] as String
val title = data["title"] as String
val lastPushMessage = PushMessage(title, message, image, ejson, count, notId, summaryText, style)
// We should use Timber here
if (BuildConfig.DEBUG) {
Log.d(PushMessage::class.java.simpleName, lastPushMessage.toString())
}
showNotification(appContext, lastPushMessage)
}
/**
* Clear all messages received to a given host the user is signed-in.
*/
fun clearNotificationsByHost(host: String) {
hostToPushMessageList.remove(host)
}
fun clearNotificationsByNotificationId(notificationId: Int) {
if (hostToPushMessageList.isNotEmpty()) {
for (entry in hostToPushMessageList.entries) {
entry.value.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
fun clearNotificationsByHostAndNotificationId(host: String, notificationId: Int) {
if (hostToPushMessageList.isNotEmpty()) {
val notifications = hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
private fun isAndroidVersionAtLeast(minVersion: Int) = Build.VERSION.SDK_INT >= minVersion
private fun getGroupForHost(host: String): TupleGroupIdMessageCount {
val size = groupMap.size
var group = groupMap.get(host)
if (group == null) {
group = TupleGroupIdMessageCount(size + 1, AtomicInteger(0))
groupMap.put(host, group)
}
return group
}
private fun showNotification(context: Context, lastPushMessage: PushMessage) {
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notId = lastPushMessage.notificationId.toInt()
val host = lastPushMessage.host
val groupTuple = getGroupForHost(host)
groupTuple.second.incrementAndGet()
val notIdListForHostname: MutableList<PushMessage>? = hostToPushMessageList.get(host)
if (notIdListForHostname == null) {
hostToPushMessageList.put(host, arrayListOf(lastPushMessage))
} else {
notIdListForHostname.add(0, lastPushMessage)
}
if (isAndroidVersionAtLeast(Build.VERSION_CODES.N)) {
val notification = createSingleNotificationForNougatAndAbove(context, lastPushMessage)
val groupNotification = createGroupNotificationForNougatAndAbove(context, lastPushMessage)
manager.notify(notId, notification)
manager.notify(groupTuple.first, groupNotification)
} else {
val notification = createSingleNotification(context, lastPushMessage)
val pushMessageList = hostToPushMessageList.get(host)
NotificationManagerCompat.from(context).notify(notId, notification)
pushMessageList?.let {
if (pushMessageList.size > 1) {
val groupNotification = createGroupNotification(context, lastPushMessage)
NotificationManagerCompat.from(context).notify(groupTuple.first, groupNotification)
}
}
}
}
private fun createGroupNotification(context: Context, lastPushMessage: PushMessage): Notification {
with(lastPushMessage) {
val id = lastPushMessage.notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = NotificationCompat.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(true)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setMessageNotification()
val subText = RocketChatCache(context).getHostSiteName(host)
if (subText.isNotEmpty()) {
builder.setSubText(subText)
}
if (style == "inbox") {
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val messageCount = pushMessageList.size
val summary = summaryText.replace("%n%", messageCount.toString())
.fromHtml()
builder.setNumber(messageCount)
if (messageCount > 1) {
val firstPush = pushMessageList[0]
val singleConversation = pushMessageList.filter {
firstPush.sender.username != it.sender.username
}.isEmpty()
val inbox = NotificationCompat.InboxStyle()
.setBigContentTitle(if (singleConversation) title else summary)
for (push in pushMessageList) {
if (singleConversation) {
inbox.addLine(push.message)
} else {
inbox.addLine("<font color='black'>${push.title}</font> <font color='gray'>${push.message}</font>".fromHtml())
}
}
builder.setStyle(inbox)
} else {
val bigText = NotificationCompat.BigTextStyle()
.bigText(pushMessageList[0].message.fromHtml())
.setBigContentTitle(pushMessageList[0].title.fromHtml())
builder.setStyle(bigText)
}
}
} else {
val bigText = NotificationCompat.BigTextStyle()
.bigText(message.fromHtml())
.setBigContentTitle(title.fromHtml())
builder.setStyle(bigText)
}
return builder.build()
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun createGroupNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification {
with(lastPushMessage) {
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage, grouped = true)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = Notification.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(true)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setMessageNotification(context)
if (isAndroidVersionAtLeast(Build.VERSION_CODES.O)) {
builder.setChannelId(host)
val groupChannel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH)
groupChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
groupChannel.enableLights(false)
groupChannel.enableVibration(true)
groupChannel.setShowBadge(true)
manager.createNotificationChannel(groupChannel)
}
val subText = RocketChatCache(context).getHostSiteName(host)
if (subText.isNotEmpty()) {
builder.setSubText(subText)
}
if (style == "inbox") {
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val count = pushMessageList.filter {
it.title == title
}.size
builder.setContentTitle(getTitle(count, title))
val inbox = Notification.InboxStyle()
.setBigContentTitle(getTitle(count, title))
for (push in pushMessageList) {
inbox.addLine(push.message)
}
builder.setStyle(inbox)
}
} else {
val bigText = Notification.BigTextStyle()
.bigText(message.fromHtml())
.setBigContentTitle(title.fromHtml())
builder.setStyle(bigText)
}
return builder.build()
}
}
private fun createSingleNotification(context: Context, lastPushMessage: PushMessage): Notification {
with(lastPushMessage) {
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = NotificationCompat.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroupSummary(false)
.setGroup(host)
.setDeleteIntent(deleteIntent)
.setContentIntent(contentIntent)
.setMessageNotification()
val subText = RocketChatCache(context).getHostSiteName(lastPushMessage.host)
if (subText.isNotEmpty()) {
builder.setSubText(subText)
}
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
if (pushMessageList.isNotEmpty()) {
val messageCount = pushMessageList.size
val bigText = NotificationCompat.BigTextStyle()
.bigText(pushMessageList.last().message.fromHtml())
.setBigContentTitle(pushMessageList.last().title.fromHtml())
builder.setStyle(bigText).setNumber(messageCount)
}
}
return builder.build()
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun createSingleNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification {
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
with(lastPushMessage) {
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = Notification.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(false)
.setDeleteIntent(deleteIntent)
.setContentIntent(contentIntent)
.setMessageNotification(context)
.addReplyAction(context, lastPushMessage)
if (isAndroidVersionAtLeast(android.os.Build.VERSION_CODES.O)) {
builder.setChannelId(host)
val channel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.enableLights(false)
channel.enableVibration(true)
channel.setShowBadge(true)
manager.createNotificationChannel(channel)
}
val subText = RocketChatCache(context).getHostSiteName(lastPushMessage.host)
if (subText.isNotEmpty()) {
builder.setSubText(subText)
}
if ("inbox" == style) {
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val userMessages = pushMessageList.filter {
it.notificationId == lastPushMessage.notificationId
}
val count = pushMessageList.filter {
it.title == title
}.size
builder.setContentTitle(getTitle(count, title))
if (count > 1) {
val inbox = Notification.InboxStyle()
inbox.setBigContentTitle(getTitle(count, title))
for (push in userMessages) {
inbox.addLine(push.message)
}
builder.setStyle(inbox)
} else {
val bigTextStyle = Notification.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
}
}
} else {
val bigTextStyle = Notification.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
}
return builder.build()
}
}
private fun getTitle(messageCount: Int, title: String): CharSequence {
return if (messageCount > 1) "($messageCount) ${title.fromHtml()}" else title.fromHtml()
}
private fun getDismissIntent(context: Context, pushMessage: PushMessage): PendingIntent {
val deleteIntent = Intent(context, DeleteReceiver::class.java)
.putExtra(EXTRA_NOT_ID, pushMessage.notificationId.toInt())
.putExtra(EXTRA_HOSTNAME, pushMessage.host)
return PendingIntent.getBroadcast(context, pushMessage.notificationId.toInt(), deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
private fun getContentIntent(context: Context, notificationId: Int, pushMessage: PushMessage, grouped: Boolean = false): PendingIntent {
val notificationIntent = Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(EXTRA_NOT_ID, notificationId)
.putExtra(EXTRA_HOSTNAME, pushMessage.host)
if (!grouped) {
notificationIntent.putExtra(EXTRA_ROOM_ID, pushMessage.rid)
}
return PendingIntent.getActivity(context, randomizer.nextInt(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
// CharSequence extensions
private fun CharSequence.fromHtml(): Spanned {
return Html.fromHtml(this as String)
}
//Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(ctx: Context, pushMessage: PushMessage): Notification.Builder {
val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL)
.build()
val replyIntent = Intent(ctx, ReplyReceiver::class.java)
replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable)
val pendingIntent = PendingIntent.getBroadcast(
ctx, randomizer.nextInt(), replyIntent, 0)
val replyAction =
Notification.Action.Builder(
Icon.createWithResource(ctx, R.drawable.ic_reply), REPLY_LABEL, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this
}
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.setMessageNotification(ctx: Context): Notification.Builder {
val res = ctx.resources
val smallIcon = res.getIdentifier(
"rocket_chat_notification", "drawable", ctx.packageName)
with(this, {
setAutoCancel(true)
setShowWhen(true)
setColor(res.getColor(R.color.colorRed400, ctx.theme))
setSmallIcon(smallIcon)
})
return this
}
// NotificationCompat.Builder extensions
private fun NotificationCompat.Builder.addReplyAction(pushMessage: PushMessage): NotificationCompat.Builder {
val context = this.mContext
val replyRemoteInput = RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL)
.build()
val replyIntent = Intent(context, ReplyReceiver::class.java)
replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable)
val pendingIntent = PendingIntent.getBroadcast(
context, randomizer.nextInt(), replyIntent, 0)
val replyAction =
NotificationCompat.Action.Builder(
R.drawable.ic_reply, REPLY_LABEL, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this
}
private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder {
val ctx = this.mContext
val res = ctx.resources
val smallIcon = res.getIdentifier(
"rocket_chat_notification", "drawable", ctx.packageName)
with(this, {
setAutoCancel(true)
setShowWhen(true)
setColor(ctx.resources.getColor(R.color.colorRed400))
setDefaults(Notification.DEFAULT_ALL)
setSmallIcon(smallIcon)
})
return this
}
private data class PushMessage(
val title: String,
val message: String,
val image: String?,
val ejson: String,
val count: String,
val notificationId: String,
val summaryText: String,
val style: String) : Serializable {
val host: String
val rid: String
val type: String
val name: String?
val sender: Sender
val createdAt: Long
init {
val json = JSONObject(ejson)
host = json.getString("host")
rid = json.getString("rid")
type = json.getString("type")
name = json.optString("name")
sender = Sender(json.getString("sender"))
createdAt = System.currentTimeMillis()
}
data class Sender(val sender: String) : Serializable {
val _id: String
val username: String
val name: String
init {
val json = JSONObject(sender)
_id = json.getString("_id")
username = json.getString("username")
name = json.getString("name")
}
}
}
/**
* BroadcastReceiver for dismissed notifications.
*/
class DeleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val notId = intent?.extras?.getInt(EXTRA_NOT_ID)
val host = intent?.extras?.getString(EXTRA_HOSTNAME)
if (host != null && notId != null) {
clearNotificationsByHostAndNotificationId(host, notId)
}
}
}
/**
* *EXPERIMENTAL*
*
* BroadcastReceiver for notifications' replies using Direct Reply feature (Android >= 7).
*/
class ReplyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null) {
return
}
synchronized(this) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val message: CharSequence? = extractMessage(intent)
val pushMessage = intent?.extras?.getSerializable(EXTRA_PUSH_MESSAGE) as PushMessage?
pushMessage?.let {
val userNotId = pushMessage.notificationId.toInt()
val groupTuple = groupMap.get(pushMessage.host)
val pushes = hostToPushMessageList.get(pushMessage.host)
pushes?.let {
val allMessagesFromSameUser = pushes.filter {
it.sender._id == pushMessage.sender._id
}
for (msg in allMessagesFromSameUser) {
manager.cancel(msg.notificationId.toInt())
groupTuple?.second?.decrementAndGet()
}
groupTuple?.let {
val groupNotId = groupTuple.first
val totalNot = groupTuple.second.get()
if (totalNot == 0) {
manager.cancel(groupNotId)
}
}
message?.let {
val httpUrl = HttpUrl.parse(pushMessage.host)
httpUrl?.let {
sendMessage(RocketChatCache(context).getSiteUrlFor(httpUrl.host()), message, pushMessage.rid)
}
}
}
}
}
}
private fun extractMessage(intent: Intent?): CharSequence? {
val remoteInput: Bundle? =
RemoteInput.getResultsFromIntent(intent)
return remoteInput?.getCharSequence(REMOTE_INPUT_REPLY)
}
// Just kept for reference. We should use this on rewrite with job schedulers
private fun sendMessage(hostname: String, message: CharSequence, roomId: String) {
val roomRepository = RealmRoomRepository(hostname)
val userRepository = RealmUserRepository(hostname)
val messageRepository = RealmMessageRepository(hostname)
val messageInteractor = MessageInteractor(messageRepository, roomRepository)
val singleRoom: Single<Room> = roomRepository.getById(roomId)
.filter({ it.isPresent })
.map({ it.get() })
.firstElement()
.toSingle()
val singleUser: Single<User> = userRepository.getCurrent()
.filter({ it.isPresent })
.map({ it.get() })
.firstElement()
.toSingle()
val roomUserTuple: Single<TupleRoomUser> = Single.zip(
singleRoom,
singleUser,
BiFunction { room, user -> TupleRoomUser(room, user) })
roomUserTuple.flatMap { tuple -> messageInteractor.send(tuple.first, tuple.second, message as String) }
.subscribeOn(AndroidSchedulers.from(BackgroundLooper.get()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ success ->
// Empty
}, { throwable ->
throwable.printStackTrace()
Logger.report(throwable)
})
}
}
}
\ No newline at end of file
package chat.rocket.android.push.gcm;
import com.google.android.gms.gcm.GcmListenerService;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Iterator;
import chat.rocket.android.push.PushConstants;
import chat.rocket.android.push.PushNotificationHandler;
import chat.rocket.android.push.PushManager;
@SuppressLint("NewApi")
public class GCMIntentService extends GcmListenerService implements PushConstants {
......@@ -33,9 +35,7 @@ public class GCMIntentService extends GcmListenerService implements PushConstant
extras = normalizeExtras(applicationContext, extras);
PushNotificationHandler pushNotificationHandler = new PushNotificationHandler();
pushNotificationHandler.showNotificationIfPossible(applicationContext, extras);
PushManager.INSTANCE.handle(applicationContext, extras);
}
/*
......
package chat.rocket.android.service;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
......@@ -22,4 +23,6 @@ public interface ConnectivityManagerApi {
List<ServerInfo> getServerList();
Observable<ServerConnectivity> getServerConnectivityAsObservable();
int getConnectivityState(@NonNull String hostname);
}
......@@ -4,6 +4,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
......@@ -148,6 +149,11 @@ import rx.subjects.PublishSubject;
return Observable.concat(Observable.from(getCurrentConnectivityList()), connectivitySubject);
}
@Override
public int getConnectivityState(@NonNull String hostname) {
return serverConnectivityList.get(hostname);
}
@DebugLog
private Single<Boolean> connectToServerIfNeeded(String hostname, boolean forceConnect) {
return Single.defer(() -> {
......
......@@ -105,11 +105,14 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
private Single<RocketChatWebSocketThread> getOrCreateWebSocketThread(String hostname) {
return Single.defer(() -> {
webSocketThreadLock.acquire();
if (webSocketThreads.containsKey(hostname)) {
int connectivityState = ConnectivityManager.getInstance(getApplicationContext()).getConnectivityState(hostname);
boolean isConnected = connectivityState == ServerConnectivity.STATE_CONNECTED;
if (webSocketThreads.containsKey(hostname) && isConnected) {
RocketChatWebSocketThread thread = webSocketThreads.get(hostname);
webSocketThreadLock.release();
return Single.just(thread);
}
connectivityManager.notifyConnecting(hostname);
return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> {
webSocketThreads.put(hostname, thread);
......
......@@ -211,6 +211,8 @@ public class RocketChatWebSocketThread extends HandlerThread {
if (task.isFaulted()) {
Exception error = task.getError();
RCLog.e(error);
connectivityManager.notifyConnectionLost(
hostname, ConnectivityManagerInternal.REASON_NETWORK_ERROR);
emitter.onError(error);
} else {
keepAliveTimer.update();
......@@ -345,7 +347,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
}
private Task<Void> fetchPublicSettings() {
return new MethodCallHelper(realmHelper, ddpClientRef).getPublicSettings();
return new MethodCallHelper(appContext, realmHelper, ddpClientRef).getPublicSettings(hostname);
}
private Task<Void> fetchPermissions() {
......
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