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 @@ ...@@ -76,6 +76,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">
......
...@@ -17,17 +17,8 @@ import chat.rocket.android.helper.MessageParser ...@@ -17,17 +17,8 @@ import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush import chat.rocket.android.push.GroupedPush
import chat.rocket.android.server.domain.AccountsRepository import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.ChatRoomsRepository import chat.rocket.android.server.domain.*
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.server.infraestructure.MemoryChatRoomsRepository import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository import chat.rocket.android.server.infraestructure.MemoryRoomRepository
...@@ -270,9 +261,22 @@ class AppModule { ...@@ -270,9 +261,22 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi) SharedPreferencesAccountsRepository(preferences, moshi)
@Provides @Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService() fun provideNotificationManager(context: Context): NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides @Provides
@Singleton @Singleton
fun provideGroupedPush() = GroupedPush() 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 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.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 ...@@ -10,6 +10,8 @@ import android.content.Intent
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
...@@ -23,10 +25,12 @@ import chat.rocket.android.server.domain.GetSettingsInteractor ...@@ -23,10 +25,12 @@ 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
...@@ -37,12 +41,12 @@ import javax.inject.Inject ...@@ -37,12 +41,12 @@ import javax.inject.Inject
* for old source code. * for old source code.
*/ */
class PushManager @Inject constructor( class PushManager @Inject constructor(
private val groupedPushes: GroupedPush, private val groupedPushes: GroupedPush,
private val manager: NotificationManager, private val manager: NotificationManager,
private val moshi: Moshi, private val moshi: Moshi,
private val getAccountInteractor: GetAccountInteractor, private val getAccountInteractor: GetAccountInteractor,
private val getSettingsInteractor: GetSettingsInteractor, private val getSettingsInteractor: GetSettingsInteractor,
private val context: Context private val context: Context
) { ) {
private val randomizer = Random() private val randomizer = Random()
...@@ -77,7 +81,7 @@ class PushManager @Inject constructor( ...@@ -77,7 +81,7 @@ class PushManager @Inject constructor(
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
private suspend fun showNotification(pushMessage: PushMessage) { suspend fun showNotification(pushMessage: PushMessage) {
if (!hasAccount(pushMessage.info.host)) { if (!hasAccount(pushMessage.info.host)) {
Timber.d("ignoring push message: $pushMessage") Timber.d("ignoring push message: $pushMessage")
return return
...@@ -209,7 +213,8 @@ class PushManager @Inject constructor( ...@@ -209,7 +213,8 @@ class PushManager @Inject constructor(
builder.setStyle(bigTextStyle) builder.setStyle(bigTextStyle)
} }
return builder.build() return builder.addReplyAction(pushMessage)
.build()
} }
} }
...@@ -279,46 +284,54 @@ class PushManager @Inject constructor( ...@@ -279,46 +284,54 @@ class PushManager @Inject constructor(
return Html.fromHtml(this as String) 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 // 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 val pendingIntent = getReplyPendingIntent(pushMessage)
// val replyIntent = Intent(context, ReplyReceiver::class.java) val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply_black_24px, replyTextHint, pendingIntent)
// replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable) .addRemoteInput(replyRemoteInput)
// val pendingIntent = PendingIntent.getBroadcast( .setAllowGeneratedReplies(true)
// context, randomizer.nextInt(), replyIntent, 0) .build()
// val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, REPLY_LABEL, pendingIntent)
// .addRemoteInput(replyRemoteInput) this.addAction(replyAction)
// .setAllowGeneratedReplies(true)
// .build()
//
// this.addAction(replyAction)
return this 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 { private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val res = context.resources val res = context.resources
...@@ -337,30 +350,72 @@ class PushManager @Inject constructor( ...@@ -337,30 +350,72 @@ class PushManager @Inject constructor(
} }
data class PushMessage( data class PushMessage(
val title: String, val title: String,
val message: String, val message: String,
val info: PushInfo, val info: PushInfo,
val image: String? = null, val image: String? = null,
val count: String? = null, val count: String? = null,
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 +424,65 @@ data class PushInfo( ...@@ -369,18 +424,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"
...@@ -181,4 +181,9 @@ ...@@ -181,4 +181,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
...@@ -181,4 +181,9 @@ ...@@ -181,4 +181,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
...@@ -182,4 +182,9 @@ ...@@ -182,4 +182,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