Commit aa0f574b authored by Leonardo Aramaki's avatar Leonardo Aramaki

Implement direct reply on Android N and above

parent 6f0cc896
...@@ -85,6 +85,15 @@ ...@@ -85,6 +85,15 @@
</intent-filter> </intent-filter>
</receiver> </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 <service
android:name=".push.FirebaseTokenService" android:name=".push.FirebaseTokenService"
android:exported="false"> android:exported="false">
......
package chat.rocket.android.dagger.module package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver 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 chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -10,4 +12,7 @@ abstract class ReceiverBuilder { ...@@ -10,4 +12,7 @@ abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class]) @ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver 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 ...@@ -7,9 +7,13 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.media.RingtoneManager import android.media.RingtoneManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.support.annotation.RequiresApi import android.support.annotation.RequiresApi
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat import android.support.v4.app.NotificationManagerCompat
...@@ -17,16 +21,17 @@ import android.support.v4.app.RemoteInput ...@@ -17,16 +21,17 @@ import android.support.v4.app.RemoteInput
import android.text.Html import android.text.Html
import android.text.Spanned import android.text.Spanned
import chat.rocket.android.R 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.GetAccountInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.siteName import chat.rocket.android.server.domain.siteName
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking import kotlinx.coroutines.experimental.runBlocking
import se.ansman.kotshi.JsonSerializable import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiConstructor
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
...@@ -209,7 +214,8 @@ class PushManager @Inject constructor( ...@@ -209,7 +214,8 @@ class PushManager @Inject constructor(
builder.setStyle(bigTextStyle) builder.setStyle(bigTextStyle)
} }
return builder.build() return builder.addReplyAction(pushMessage)
.build()
} }
} }
...@@ -282,40 +288,49 @@ class PushManager @Inject constructor( ...@@ -282,40 +288,49 @@ class PushManager @Inject constructor(
//Notification.Builder extensions //Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(pushMessage: PushMessage): Notification.Builder { 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) val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL) .setLabel(replyTextHint)
.build() .build()
//TODO: Implement this when we have sendMessage call //TODO: Implement this when we have sendMessage call
// val replyIntent = Intent(context, ReplyReceiver::class.java) val replyIntent = Intent(context, DirectReplyReceiver::class.java)
// replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable) replyIntent.action = ACTION_REPLY
// val pendingIntent = PendingIntent.getBroadcast( replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Parcelable)
// context, randomizer.nextInt(), replyIntent, 0) val filter = IntentFilter().apply {
// val replyAction = addAction(ACTION_REPLY)
// Notification.Action.Builder( }
// Icon.createWithResource(context, R.drawable.ic_reply), REPLY_LABEL, pendingIntent) val pendingIntent = PendingIntent.getBroadcast(
// .addRemoteInput(replyRemoteInput) context, randomizer.nextInt(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
// .setAllowGeneratedReplies(true) val replyAction = Notification.Action.Builder(
// .build() Icon.createWithResource(context, R.drawable.ic_reply_black_24px), replyTextHint, pendingIntent)
// this.addAction(replyAction) .addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this return this
} }
// NotificationCompat.Builder extensions // NotificationCompat.Builder extensions
private fun NotificationCompat.Builder.addReplyAction(pushMessage: PushMessage): NotificationCompat.Builder { 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) val replyRemoteInput = RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL) .setLabel(replyTextHint)
.build() .build()
//TODO: Implement when we have sendMessage call //TODO: Implement when we have sendMessage call
// val replyIntent = Intent(context, ReplyReceiver::class.java) val replyIntent = Intent(context, DirectReplyReceiver::class.java)
// replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable) replyIntent.action = ACTION_REPLY
// val pendingIntent = PendingIntent.getBroadcast( replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Parcelable)
// context, randomizer.nextInt(), replyIntent, 0) val filter = IntentFilter().apply {
// val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, REPLY_LABEL, pendingIntent) addAction(ACTION_REPLY)
// .addRemoteInput(replyRemoteInput) }
// .setAllowGeneratedReplies(true) val pendingIntent = PendingIntent.getBroadcast(
// .build() context, randomizer.nextInt(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
// val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply_black_24px, replyTextHint, pendingIntent)
// this.addAction(replyAction) .addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this return this
} }
...@@ -345,22 +360,64 @@ data class PushMessage( ...@@ -345,22 +360,64 @@ data class PushMessage(
val notificationId: String, val notificationId: String,
val summaryText: String? = null, val summaryText: String? = null,
val style: 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 @JsonSerializable
data class PushInfo( data class PushInfo @KotshiConstructor constructor(
@Json(name = "host") val hostname: String, @Json(name = "host") val hostname: String,
@Json(name = "rid") val roomId: String, @Json(name = "rid") val roomId: String,
val type: RoomType, val type: RoomType,
val name: String?, val name: String?,
val sender: PushSender? val sender: PushSender?
) { ) : Parcelable {
val createdAt: Long val createdAt: Long
get() = System.currentTimeMillis() get() = System.currentTimeMillis()
val host by lazy { val host by lazy {
sanitizeUrl(hostname) 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 { private fun sanitizeUrl(baseUrl: String): String {
var url = baseUrl.trim() var url = baseUrl.trim()
while (url.endsWith('/')) { while (url.endsWith('/')) {
...@@ -369,18 +426,65 @@ data class PushInfo( ...@@ -369,18 +426,65 @@ data class PushInfo(
return url 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 @JsonSerializable
data class PushSender( data class PushSender @KotshiConstructor constructor(
@Json(name = "_id") val id: String, @Json(name = "_id") val id: String,
val username: String?, val username: String?,
val name: 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_NOT_ID = "chat.rocket.android.EXTRA_NOT_ID"
const val EXTRA_HOSTNAME = "chat.rocket.android.EXTRA_HOSTNAME" const val EXTRA_HOSTNAME = "chat.rocket.android.EXTRA_HOSTNAME"
const val EXTRA_PUSH_MESSAGE = "chat.rocket.android.EXTRA_PUSH_MESSAGE" const val EXTRA_PUSH_MESSAGE = "chat.rocket.android.EXTRA_PUSH_MESSAGE"
const val EXTRA_ROOM_ID = "chat.rocket.android.EXTRA_ROOM_ID" const val EXTRA_ROOM_ID = "chat.rocket.android.EXTRA_ROOM_ID"
private const val REPLY_LABEL = "REPLY" const val ACTION_REPLY = "chat.rocket.android.ACTION_REPLY"
private const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY" const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY"
...@@ -177,4 +177,9 @@ ...@@ -177,4 +177,9 @@
<string name="header_direct_messages">प्रत्यक्ष संदेश</string> <string name="header_direct_messages">प्रत्यक्ष संदेश</string>
<string name="header_live_chats">Live Chats</string> <string name="header_live_chats">Live Chats</string>
<string name="header_unknown">अज्ञात</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> </resources>
\ No newline at end of file
...@@ -177,4 +177,9 @@ ...@@ -177,4 +177,9 @@
<string name="header_direct_messages">Mensagens diretas</string> <string name="header_direct_messages">Mensagens diretas</string>
<string name="header_live_chats">Live Chats</string> <string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Desconhecido</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> </resources>
\ No newline at end of file
...@@ -178,4 +178,9 @@ ...@@ -178,4 +178,9 @@
<string name="header_direct_messages">Direct Messages</string> <string name="header_direct_messages">Direct Messages</string>
<string name="header_live_chats">Live Chats</string> <string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Unknown</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> </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