Commit aa0f574b authored by Leonardo Aramaki's avatar Leonardo Aramaki

Implement direct reply on Android N and above

parent 6f0cc896
......@@ -85,6 +85,15 @@
</intent-filter>
</receiver>
<receiver
android:name=".push.DirectReplyReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="chat.rocket.android.ACTION_REPLY" />
</intent-filter>
</receiver>
<service
android:name=".push.FirebaseTokenService"
android:exported="false">
......
package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver
import chat.rocket.android.push.DirectReplyReceiver
import chat.rocket.android.push.DirectReplyReceiverProvider
import chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -10,4 +12,7 @@ abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver
@ContributesAndroidInjector(modules = [DirectReplyReceiverProvider::class])
abstract fun bindDirectReplyReceiver(): DirectReplyReceiver
}
\ No newline at end of file
package chat.rocket.android.push
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.app.RemoteInput
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import java.util.*
import javax.inject.Inject
/**
* BroadcastReceiver for direct reply on notifications.
*/
class DirectReplyReceiver : BroadcastReceiver() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var groupedPushes: GroupedPush
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
if (ACTION_REPLY == intent.action) {
val message = intent.getParcelableExtra<PushMessage>(EXTRA_PUSH_MESSAGE)
message?.let {
launch(UI) {
try {
println(it)
sendMessage(it, extractReplyMessage(intent))
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
clearNotificationsByHostAndNotificationId(it.info.host, it.notificationId.toInt())
manager.cancel(it.notificationId.toInt())
val feedback = context.getString(R.string.notif_success_sending, it.title)
Toast.makeText(context, feedback, Toast.LENGTH_SHORT).show()
} catch (ex: RocketChatException) {
Timber.e(ex)
val feedback = context.getString(R.string.notif_error_sending)
Toast.makeText(context, feedback, Toast.LENGTH_SHORT).show()
}
}
}
}
}
private suspend fun sendMessage(message: PushMessage, replyText: CharSequence?) {
replyText?.let { reply ->
val currentServer = message.info.hostname
val roomId = message.info.roomId
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
val id = UUID.randomUUID().toString()
client.sendMessage(id, roomId, reply.toString())
// Do we need to disconnect here?
}
}
private fun extractReplyMessage(intent: Intent): CharSequence? {
val bundle = RemoteInput.getResultsFromIntent(intent)
if (bundle != null) {
return bundle.getCharSequence(REMOTE_INPUT_REPLY)
}
return null
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
private fun clearNotificationsByHostAndNotificationId(host: String, notificationId: Int) {
if (groupedPushes.hostToPushMessageList.isNotEmpty()) {
val notifications = groupedPushes.hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.push
import chat.rocket.android.dagger.module.AppModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class DirectReplyReceiverProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideDirectReplyReceiver(): DirectReplyReceiver
}
\ No newline at end of file
......@@ -7,9 +7,13 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.media.RingtoneManager
import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.support.annotation.RequiresApi
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
......@@ -17,16 +21,17 @@ import android.support.v4.app.RemoteInput
import android.text.Html
import android.text.Spanned
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.siteName
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking
import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiConstructor
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
......@@ -209,7 +214,8 @@ class PushManager @Inject constructor(
builder.setStyle(bigTextStyle)
}
return builder.build()
return builder.addReplyAction(pushMessage)
.build()
}
}
......@@ -282,40 +288,49 @@ class PushManager @Inject constructor(
//Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(pushMessage: PushMessage): Notification.Builder {
val replyTextHint = context.getText(R.string.notif_action_reply_hint)
val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL)
.setLabel(replyTextHint)
.build()
//TODO: Implement this when we have sendMessage call
// 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 =
// Notification.Action.Builder(
// Icon.createWithResource(context, R.drawable.ic_reply), REPLY_LABEL, pendingIntent)
// .addRemoteInput(replyRemoteInput)
// .setAllowGeneratedReplies(true)
// .build()
// this.addAction(replyAction)
val replyIntent = Intent(context, DirectReplyReceiver::class.java)
replyIntent.action = ACTION_REPLY
replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Parcelable)
val filter = IntentFilter().apply {
addAction(ACTION_REPLY)
}
val pendingIntent = PendingIntent.getBroadcast(
context, randomizer.nextInt(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val replyAction = Notification.Action.Builder(
Icon.createWithResource(context, R.drawable.ic_reply_black_24px), replyTextHint, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this
}
// NotificationCompat.Builder extensions
private fun NotificationCompat.Builder.addReplyAction(pushMessage: PushMessage): NotificationCompat.Builder {
val replyTextHint = context.getText(R.string.notif_action_reply_hint)
val replyRemoteInput = RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL)
.setLabel(replyTextHint)
.build()
//TODO: Implement when we have sendMessage call
// 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)
val replyIntent = Intent(context, DirectReplyReceiver::class.java)
replyIntent.action = ACTION_REPLY
replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Parcelable)
val filter = IntentFilter().apply {
addAction(ACTION_REPLY)
}
val pendingIntent = PendingIntent.getBroadcast(
context, randomizer.nextInt(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply_black_24px, replyTextHint, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this
}
......@@ -345,22 +360,64 @@ data class PushMessage(
val notificationId: String,
val summaryText: String? = null,
val style: String? = null
)
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readParcelable(PushMessage::class.java.classLoader),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeString(message)
parcel.writeParcelable(info, flags)
parcel.writeString(image)
parcel.writeString(count)
parcel.writeString(notificationId)
parcel.writeString(summaryText)
parcel.writeString(style)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PushMessage> {
override fun createFromParcel(parcel: Parcel): PushMessage {
return PushMessage(parcel)
}
override fun newArray(size: Int): Array<PushMessage?> {
return arrayOfNulls(size)
}
}
}
@JsonSerializable
data class PushInfo(
data class PushInfo @KotshiConstructor constructor(
@Json(name = "host") val hostname: String,
@Json(name = "rid") val roomId: String,
val type: RoomType,
val name: String?,
val sender: PushSender?
) {
) : Parcelable {
val createdAt: Long
get() = System.currentTimeMillis()
val host by lazy {
sanitizeUrl(hostname)
}
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
roomTypeOf(parcel.readString()),
parcel.readString(),
parcel.readParcelable(PushInfo::class.java.classLoader))
private fun sanitizeUrl(baseUrl: String): String {
var url = baseUrl.trim()
while (url.endsWith('/')) {
......@@ -369,18 +426,65 @@ data class PushInfo(
return url
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(hostname)
parcel.writeString(roomId)
parcel.writeString(type.toString())
parcel.writeString(name)
parcel.writeParcelable(sender, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PushInfo> {
override fun createFromParcel(parcel: Parcel): PushInfo {
return PushInfo(parcel)
}
override fun newArray(size: Int): Array<PushInfo?> {
return arrayOfNulls(size)
}
}
}
@JsonSerializable
data class PushSender(
data class PushSender @KotshiConstructor constructor(
@Json(name = "_id") val id: String,
val username: String?,
val name: String?
)
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id)
parcel.writeString(username)
parcel.writeString(name)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PushSender> {
override fun createFromParcel(parcel: Parcel): PushSender {
return PushSender(parcel)
}
override fun newArray(size: Int): Array<PushSender?> {
return arrayOfNulls(size)
}
}
}
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"
const val ACTION_REPLY = "chat.rocket.android.ACTION_REPLY"
const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY"
......@@ -177,4 +177,9 @@
<string name="header_direct_messages">प्रत्यक्ष संदेश</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">अज्ञात</string>
<!--Notifications-->
<string name="notif_action_reply_hint">जवाब</string>
<string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string>
<string name="notif_success_sending">संदेश भेजा गया %1$s!</string>
</resources>
\ No newline at end of file
......@@ -177,4 +177,9 @@
<string name="header_direct_messages">Mensagens diretas</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Desconhecido</string>
<!--Notifications-->
<string name="notif_action_reply_hint">RESPONDER</string>
<string name="notif_error_sending">Falha ao enviar a mensagem.</string>
<string name="notif_success_sending">Mensagem enviada para %1$s!</string>
</resources>
\ No newline at end of file
......@@ -178,4 +178,9 @@
<string name="header_direct_messages">Direct Messages</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Unknown</string>
<!--Notifications-->
<string name="notif_action_reply_hint">REPLY</string>
<string name="notif_error_sending">Reply has failed. Please try again.</string>
<string name="notif_success_sending">Message sent to %1$s!</string>
</resources>
\ No newline at end of file
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