Unverified Commit d8219843 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #1038 from RocketChat/new/direct-reply

[NEW] Direct Reply from notification
parents 18e1cf9e 3e7984fd
......@@ -76,6 +76,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">
......
......@@ -17,17 +17,8 @@ import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
......@@ -270,9 +261,22 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
fun provideNotificationManager(context: Context): NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides
@Singleton
fun provideGroupedPush() = GroupedPush()
@Provides
@Singleton
fun providePushManager(
context: Context,
groupedPushes: GroupedPush,
manager: NotificationManager,
moshi: Moshi,
getAccountInteractor: GetAccountInteractor,
getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
}
}
\ No newline at end of file
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.NotificationManagerCompat
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
@Inject
lateinit var pushManager: PushManager
@Inject
lateinit var manager: NotificationManager
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) {
val notificationId = it.notificationId.toInt()
val hostname = it.info.host
try {
sendMessage(it, extractReplyMessage(intent))
clearNotificationsByHostAndNotificationId(hostname, notificationId)
manager.cancel(notificationId)
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()
clearNotificationsByHostAndNotificationId(hostname, notificationId)
pushManager.showNotification(it)
}
}
}
}
}
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
......@@ -10,6 +10,8 @@ import android.content.Intent
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
......@@ -23,10 +25,12 @@ 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
......@@ -37,12 +41,12 @@ import javax.inject.Inject
* for old source code.
*/
class PushManager @Inject constructor(
private val groupedPushes: GroupedPush,
private val manager: NotificationManager,
private val moshi: Moshi,
private val getAccountInteractor: GetAccountInteractor,
private val getSettingsInteractor: GetSettingsInteractor,
private val context: Context
private val groupedPushes: GroupedPush,
private val manager: NotificationManager,
private val moshi: Moshi,
private val getAccountInteractor: GetAccountInteractor,
private val getSettingsInteractor: GetSettingsInteractor,
private val context: Context
) {
private val randomizer = Random()
......@@ -77,7 +81,7 @@ class PushManager @Inject constructor(
}
@SuppressLint("NewApi")
private suspend fun showNotification(pushMessage: PushMessage) {
suspend fun showNotification(pushMessage: PushMessage) {
if (!hasAccount(pushMessage.info.host)) {
Timber.d("ignoring push message: $pushMessage")
return
......@@ -209,7 +213,8 @@ class PushManager @Inject constructor(
builder.setStyle(bigTextStyle)
}
return builder.build()
return builder.addReplyAction(pushMessage)
.build()
}
}
......@@ -279,46 +284,54 @@ class PushManager @Inject constructor(
return Html.fromHtml(this as String)
}
//Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(pushMessage: PushMessage): Notification.Builder {
val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL)
.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)
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 pendingIntent = getReplyPendingIntent(pushMessage)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply_black_24px, replyTextHint, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
this.addAction(replyAction)
return this
}
private fun getReplyIntent(pushMessage: PushMessage): Intent {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Intent(context, DirectReplyReceiver::class.java)
} else {
Intent(context, MainActivity::class.java).also {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}.also {
it.action = ACTION_REPLY
it.putExtra(EXTRA_PUSH_MESSAGE, pushMessage)
}
}
private fun getReplyPendingIntent(pushMessage: PushMessage): PendingIntent {
val replyIntent = getReplyIntent(pushMessage)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PendingIntent.getBroadcast(
context,
randomizer.nextInt(),
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
context,
randomizer.nextInt(),
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val res = context.resources
......@@ -337,30 +350,72 @@ class PushManager @Inject constructor(
}
data class PushMessage(
val title: String,
val message: String,
val info: PushInfo,
val image: String? = null,
val count: String? = null,
val notificationId: String,
val summaryText: String? = null,
val style: String? = null
)
val title: String,
val message: String,
val info: PushInfo,
val image: String? = null,
val count: String? = null,
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(
@Json(name = "host") val hostname: String,
@Json(name = "rid") val roomId: String,
val type: RoomType,
val name: String?,
val sender: PushSender?
) {
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 +424,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(
@Json(name = "_id") val id: String,
val username: String?,
val name: String?
)
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"
......@@ -181,4 +181,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
......@@ -181,4 +181,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
......@@ -182,4 +182,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