Commit 4505780c authored by Lucio Maciel's avatar Lucio Maciel

Better multi server support, support push notifications

parent d2cc3161
......@@ -3,30 +3,31 @@ package chat.rocket.android.app
import android.app.Activity
import android.app.Application
import android.app.Service
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.jakewharton.threetenabp.AndroidThreeTen
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.experimental.runBlocking
import timber.log.Timber
import javax.inject.Inject
import android.content.BroadcastReceiver
import dagger.android.*
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector {
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector {
@Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
......@@ -34,6 +35,9 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service>
@Inject
lateinit var broadcastReceiverInjector: DispatchingAndroidInjector<BroadcastReceiver>
@Inject
lateinit var imagePipelineConfig: ImagePipelineConfig
@Inject
......@@ -48,14 +52,19 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var tokenRepository: TokenRepository
@Inject
lateinit var prefs: SharedPreferences
@Inject
lateinit var getAccountsInteractor: GetAccountsInteractor
override fun onCreate() {
super.onCreate()
DaggerAppComponent.builder().application(this).build().inject(this)
// TODO - remove this when we have a proper service handling connection...
initCurrentServer()
// TODO - remove this on the future, temporary migration stuff for pre-release versions.
prefs.edit { putBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, true) }
migrateInternalTokens()
AndroidThreeTen.init(this)
EmojiRepository.load(this)
......@@ -65,14 +74,27 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
setupTimber()
}
// TODO - remove this when we have a proper service handling connection...
private fun initCurrentServer() {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
if (currentServer != null && serverToken != null && settings != null) {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
private fun migrateInternalTokens() {
if (!prefs.getBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, true)) {
Timber.d("Tokens already migrated")
return
}
getCurrentServerInteractor.get()?.let { serverUrl ->
multiServerRepository.get(serverUrl)?.let { token ->
tokenRepository.save(serverUrl, Token(token.userId, token.authToken))
}
}
runBlocking {
getAccountsInteractor.get().forEach { account ->
multiServerRepository.get(account.serverUrl)?.let { token ->
tokenRepository.save(account.serverUrl, token.toToken())
}
}
}
prefs.edit { putBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, false) }
}
private fun setupCrashlytics() {
......@@ -99,4 +121,10 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
override fun serviceInjector(): AndroidInjector<Service> {
return serviceDispatchingAndroidInjector
}
override fun broadcastReceiverInjector(): AndroidInjector<BroadcastReceiver> {
return broadcastReceiverInjector
}
}
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
\ No newline at end of file
package chat.rocket.android.authentication.domain.model
import chat.rocket.common.model.Token
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class TokenModel(val userId: String, val authToken: String)
fun TokenModel.toToken() = Token(userId, authToken)
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
class MemoryTokenRepository : TokenRepository {
var savedToken: Token? = null
override fun get(): Token? {
return savedToken
}
override fun save(token: Token) {
savedToken = token
}
}
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token
import com.squareup.moshi.Moshi
import timber.log.Timber
class SharedPreferencesTokenRepository(private val prefs: SharedPreferences, moshi: Moshi) : TokenRepository {
private var servers = prefs.getStringSet(KEY_SERVERS, emptySet()).toMutableSet()
private var currentUrl: String? = null
private var currentToken: Token? = null
private val adapter = moshi.adapter<TokenModel>(TokenModel::class.java)
override fun get(url: String): Token? {
if (currentToken != null && url == currentUrl) {
return currentToken
}
try {
prefs.getString(tokenKey(url), null)?.let { tokenStr ->
val model = adapter.fromJson(tokenStr)
model?.let {
val token = Token(model.userId, model.authToken)
currentToken = token
currentUrl = url
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error parsing token for ${tokenKey(url)}")
ex.printStackTrace()
}
return currentToken
}
override fun save(url: String, token: Token) {
try {
val model = TokenModel(token.userId, token.authToken)
val str = adapter.toJson(model)
servers.add(url)
prefs.edit {
putString(tokenKey(url), str)
putStringSet(KEY_SERVERS, servers)
}
currentToken = token
currentUrl = url
} catch (ex: Exception) {
Timber.d(ex, "Error saving token for ${tokenKey(url)}")
ex.printStackTrace()
}
}
override fun remove(url: String) {
servers.remove(url)
prefs.edit {
remove(url)
putStringSet(KEY_SERVERS, servers)
}
}
override fun clear() {
servers.forEach { server ->
prefs.edit { remove(server) }
}
servers.clear()
prefs.edit {
remove(KEY_SERVERS)
}
}
private fun tokenKey(url: String) = "$KEY_TOKEN$url"
}
private const val KEY_TOKEN = "KEY_TOKEN_"
private const val KEY_SERVERS = "KEY_SERVERS"
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmailValid
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.util.ifNull
......@@ -26,12 +27,13 @@ import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor,
private val settingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
factory: RocketChatClientFactory) {
private val factory: RocketChatClientFactory) {
// TODO - we should validate the current server when opening the app, and have a nonnull get()
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
......@@ -198,16 +200,17 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
fun signup() = navigator.toSignUp()
private suspend fun saveToken(server: String, tokenModel: TokenModel, username: String?) {
multiServerRepository.save(server, tokenModel)
localRepository.save(LocalRepository.USERNAME_KEY, username)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
tokenRepository.save(server, tokenModel.toToken())
registerPushToken()
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
......
package chat.rocket.android.authentication.presentation
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.TokenRepository
import javax.inject.Inject
class AuthenticationPresenter @Inject constructor(private val navigator: AuthenticationNavigator,
class AuthenticationPresenter @Inject constructor(
private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val multiServerRepository: MultiServerTokenRepository,
private val getAccountInteractor: GetAccountInteractor,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository) {
fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
private val localRepository: LocalRepository,
private val tokenRepository: TokenRepository
) {
suspend fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (newServer || currentServer == null || serverToken == null || settings == null) {
callback(false)
} else {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
callback(true)
navigator.toChatList()
}
......
......@@ -62,7 +62,7 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
// TODO This function returns a user token so should we save it?
client.login(username, password)
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
navigator.toChatList()
......
package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
......@@ -10,24 +9,25 @@ import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.model.Myself
import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
......@@ -54,10 +54,7 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
val me = client.me()
saveAccount(me)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -85,9 +82,10 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
......
......@@ -9,29 +9,39 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
super.onCreate(savedInstanceState)
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
if (authenticated) {
// just call onCreate, and the presenter will call the navigator...
super.onCreate(savedInstanceState)
} else {
if (!authenticated) {
showServerInput(savedInstanceState)
}
}
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
......
......@@ -97,7 +97,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.USERNAME_KEY)
val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
val resources = context.resources
......
......@@ -361,7 +361,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY)
val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self }
......@@ -408,7 +408,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
if (users.isNotEmpty()) {
usersRepository.saveAll(users)
}
val self = localRepository.get(LocalRepository.USERNAME_KEY)
val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
view.populatePeopleSuggestions(users.map {
val username = it.username ?: ""
val name = it.name ?: ""
......
......@@ -15,7 +15,6 @@ import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value
......@@ -23,6 +22,7 @@ import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl
import timber.log.Timber
......@@ -32,15 +32,17 @@ import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository,
private val getAccountInteractor: GetAccountInteractor,
tokenRepository: TokenRepository,
localRepository: LocalRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
getSettingsInteractor: GetSettingsInteractor,
localRepository: LocalRepository) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl()
private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY)
private val token = tokenRepository.get()
private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
......@@ -249,6 +251,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val quoteMessage: Message = quote
quoteViewModel = mapMessage(quoteMessage)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
......
......@@ -4,6 +4,7 @@ import android.app.Application
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
import chat.rocket.android.dagger.module.ServiceBuilder
import chat.rocket.android.push.FirebaseTokenService
import dagger.BindsInstance
......@@ -12,7 +13,8 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class, ServiceBuilder::class])
@Component(modules = [AndroidSupportInjectionModule::class,
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class])
interface AppComponent {
@Component.Builder
......
package chat.rocket.android.dagger.module
import android.app.Application
import android.app.NotificationManager
import android.arch.persistence.room.Room
import android.content.Context
import android.content.SharedPreferences
import androidx.content.systemService
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor
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.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -110,8 +113,8 @@ class AppModule {
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository): Interceptor {
return FrescoAuthInterceptor(tokenRepository)
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
@Provides
......@@ -144,8 +147,8 @@ class AppModule {
@Provides
@Singleton
fun provideTokenRepository(): TokenRepository {
return MemoryTokenRepository()
fun provideTokenRepository(prefs: SharedPreferences, moshi: Moshi): TokenRepository {
return SharedPreferencesTokenRepository(prefs, moshi)
}
@Provides
......@@ -192,7 +195,10 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.build()
}
@Provides
......@@ -245,4 +251,11 @@ class AppModule {
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
@Provides
@Singleton
fun provideGroupedPush() = GroupedPush()
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver
import chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider
import chat.rocket.android.push.di.GcmListenerServiceProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -9,4 +11,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class])
abstract fun bindFirebaseTokenService(): FirebaseTokenService
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Interceptor {
class FrescoAuthInterceptor(
private val tokenRepository: TokenRepository,
private val currentServerInteractor: GetCurrentServerInteractor
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.get()
var request = chain.request()
currentServerInteractor.get()?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
token?.let {
return@let token?.let {
val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken)
......@@ -18,7 +24,7 @@ class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Inte
url(url)
}.build()
}
}
return chain.proceed(request)
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@ interface LocalRepository {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username"
const val CURRENT_USERNAME_KEY = "username_"
}
fun save(key: String, value: String?)
......
......@@ -20,6 +20,6 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L
clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server)
clear(LocalRepository.USERNAME_KEY + server)
clear(LocalRepository.CURRENT_USERNAME_KEY)
}
}
\ No newline at end of file
......@@ -10,28 +10,32 @@ import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.unregisterPushToken
import timber.log.Timber
import javax.inject.Inject
class MainPresenter @Inject constructor(private val view: MainView,
class MainPresenter @Inject constructor(
private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val multiServerRepository: MultiServerTokenRepository,
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory,
factory: RocketChatClientFactory) {
managerFactory: ConnectionManagerFactory
) {
private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
......@@ -79,7 +83,7 @@ class MainPresenter @Inject constructor(private val view: MainView,
client.logout()
disconnect()
removeAccountInterector.remove(currentServer)
multiServerRepository.clear(currentServer)
tokenRepository.remove(currentServer)
navigator.toNewServer()
} catch (exception: RocketChatException) {
exception.message?.let {
......@@ -120,4 +124,10 @@ class MainPresenter @Inject constructor(private val view: MainView,
fun addNewServer() {
navigator.toServerScreen()
}
suspend fun refreshToken(token: String?) {
token?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
}
}
\ No newline at end of file
package chat.rocket.android.main.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
......@@ -10,7 +9,6 @@ import android.view.Gravity
import android.view.MenuItem
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.adapter.AccountSelector
import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.presentation.MainPresenter
......@@ -21,6 +19,8 @@ import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast
import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -29,6 +29,8 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.nav_header.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -43,6 +45,12 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(CommonPool) {
val token = InstanceID.getInstance(this@MainActivity).getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
Timber.d("GCM token: $token")
presenter.refreshToken(token)
}
presenter.connect()
presenter.loadCurrentInfo()
setupToolbar()
......
package chat.rocket.android.push
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.android.AndroidInjection
import javax.inject.Inject
/**
* BroadcastReceiver for dismissed notifications.
*/
class DeleteReceiver : BroadcastReceiver() {
@Inject
lateinit var groupedPushes: GroupedPush
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
val notId = intent.extras?.getInt(EXTRA_NOT_ID)
val host = intent.extras?.getString(EXTRA_HOSTNAME)
if (host != null && notId != null) {
clearNotificationsByHostAndNotificationId(host, notId)
}
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
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
......@@ -2,6 +2,8 @@ package chat.rocket.android.push
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken
......@@ -16,14 +18,16 @@ import javax.inject.Inject
class FirebaseTokenService : FirebaseInstanceIdService() {
@Inject
lateinit var client: RocketChatClient
lateinit var factory: RocketChatClientFactory
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject
lateinit var localRepository: LocalRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this);
AndroidInjection.inject(this)
}
override fun onTokenRefresh() {
......@@ -31,11 +35,14 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
// default push gateway. We should register this project's own project sender id into it.
val gcmToken = InstanceID.getInstance(this)
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get()!!
val client = factory.create(currentServer)
gcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken)
launch {
try {
Timber.d("Registering push token: $gcmToken for ${client.url}")
client.registerPushToken(gcmToken)
} catch (ex: RocketChatException) {
Timber.e(ex)
......
......@@ -2,12 +2,22 @@ package chat.rocket.android.push
import android.os.Bundle
import com.google.android.gms.gcm.GcmListenerService
import dagger.android.AndroidInjection
import javax.inject.Inject
class GcmListenerService : GcmListenerService() {
@Inject
lateinit var pushManager: PushManager
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onMessageReceived(from: String?, data: Bundle?) {
data?.let {
PushManager.handle(this, data)
pushManager.handle(data)
}
}
}
\ No newline at end of file
package chat.rocket.android.push
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Singleton
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
class GroupedPush {
// Notifications received from the same server are grouped in a single bundled notification.
// This map associates a host to a group id.
val groupMap = HashMap<String, TupleGroupIdMessageCount>()
// Map a hostname to a list of push messages that pertain to it.
val hostToPushMessageList = HashMap<String, MutableList<PushMessage>>()
}
\ No newline at end of file
......@@ -5,7 +5,6 @@ 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.media.RingtoneManager
......@@ -17,36 +16,34 @@ 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.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import org.json.JSONObject
import java.io.Serializable
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 com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking
import se.ansman.kotshi.JsonSerializable
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.HashMap
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
import javax.inject.Inject
/**
* Refer to: https://github.com/RocketChat/Rocket.Chat.Android/blob/9e846b7fde8fe0c74b9e0117c37ce49293308db5/app/src/main/java/chat/rocket/android/push/PushManager.kt
* for old source code.
*/
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>>()
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 randomizer = Random()
/**
......@@ -54,187 +51,108 @@ object PushManager {
* on the *data* param bundle received.
*/
@Synchronized
fun handle(context: Context, data: Bundle) {
val appContext = context.applicationContext
fun handle(data: Bundle) = runBlocking {
val message = data["message"] as String?
val image = data["image"] as String?
val ejson = data["ejson"] as String?
val title = data["title"] as String?
val notId = data["notId"] as String? ?: randomizer.nextInt().toString()
val image = data["image"] 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?
if (ejson == null || message == null || title == null) {
return
}
try {
val adapter = moshi.adapter<PushInfo>(PushInfo::class.java)
val info = adapter.fromJson(ejson)
val lastPushMessage = PushMessage(title, message, image, ejson, count, notId, summaryText, style)
val pushMessage = PushMessage(title!!, message!!, info!!, image, 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)
}
/**
* Remove a notification solely by it's unique id.
*/
fun clearNotificationsByNotificationId(notificationId: Int) {
if (hostToPushMessageList.isNotEmpty()) {
for (entry in hostToPushMessageList.entries) {
entry.value.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
fun clearNotificationsByHostAndNotificationId(host: String?, notificationId: Int?) {
if (host == null || notificationId == null) {
return
}
if (hostToPushMessageList.isNotEmpty()) {
val notifications = hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
Timber.d("Received push message: $pushMessage")
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)
showNotification(pushMessage)
} catch (ex: Exception) {
Timber.d(ex, "Error parsing PUSH message: $data")
ex.printStackTrace()
}
return group
}
@SuppressLint("NewApi")
internal fun showNotification(context: Context, lastPushMessage: PushMessage) {
if (lastPushMessage.host == null || lastPushMessage.message == null || lastPushMessage.title == null) {
private suspend fun showNotification(pushMessage: PushMessage) {
if (!hasAccount(pushMessage.info.host)) {
Timber.d("ignoring push message: $pushMessage")
return
}
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notId = lastPushMessage.notificationId.toInt()
val host = lastPushMessage.host
val notId = pushMessage.notificationId.toInt()
val host = pushMessage.info.host
val groupTuple = getGroupForHost(host)
groupTuple.second.incrementAndGet()
val notIdListForHostname: MutableList<PushMessage>? = hostToPushMessageList.get(host)
val notIdListForHostname: MutableList<PushMessage>? = groupedPushes.hostToPushMessageList.get(host)
if (notIdListForHostname == null) {
hostToPushMessageList.put(host, arrayListOf(lastPushMessage))
groupedPushes.hostToPushMessageList[host] = arrayListOf(pushMessage)
} else {
notIdListForHostname.add(0, lastPushMessage)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val notification = createSingleNotificationForNougatAndAbove(context, lastPushMessage)
val groupNotification = createGroupNotificationForNougatAndAbove(context, lastPushMessage)
notification?.let {
manager.notify(notId, notification)
notIdListForHostname.add(0, pushMessage)
}
groupNotification?.let {
manager.notify(groupTuple.first, groupNotification)
}
} else {
val notification = createSingleNotification(context, lastPushMessage)
val pushMessageList = hostToPushMessageList.get(host)
val notification = createSingleNotification(pushMessage)
val pushMessageList = groupedPushes.hostToPushMessageList[host]
notification?.let {
NotificationManagerCompat.from(context).notify(notId, notification)
manager.notify(notId, notification)
}
pushMessageList?.let {
if (pushMessageList.size > 1) {
val groupNotification = createGroupNotification(context, lastPushMessage)
val groupNotification = createGroupNotification(pushMessage)
groupNotification?.let {
NotificationManagerCompat.from(context).notify(groupTuple.first, groupNotification)
}
}
}
}
}
internal fun createGroupNotification(context: Context, lastPushMessage: PushMessage): Notification? {
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
private fun getGroupForHost(host: String): TupleGroupIdMessageCount {
val size = groupedPushes.groupMap.size
var group = groupedPushes.groupMap[host]
if (group == null) {
group = TupleGroupIdMessageCount(size + 1, AtomicInteger(0))
groupedPushes.groupMap[host] = group
}
return group
}
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()
//TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat"
if (subText.isNotEmpty()) {
builder.setSubText(subText)
private suspend fun hasAccount(host: String): Boolean {
return getAccountInteractor.get(host) != null
}
@SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.N)
private fun createGroupNotification(pushMessage: PushMessage): Notification? {
with(pushMessage) {
val host = info.host
val builder = createBaseNotificationBuilder(pushMessage, grouped = true)
.setGroupSummary(true)
if (style == null || style == "inbox") {
val pushMessageList = hostToPushMessageList.get(host)
val pushMessageList = groupedPushes.hostToPushMessageList[host]
pushMessageList?.let {
val messageCount = pushMessageList.size
val summary = summaryText?.replace("%n%", messageCount.toString())
?.fromHtml() ?: "$messageCount new messages"
builder.setNumber(messageCount)
if (messageCount > 1) {
val firstPush = pushMessageList[0]
val singleConversation = pushMessageList.filter {
firstPush.sender?.username != it.sender?.username
}.isEmpty()
val count = pushMessageList.filter {
it.title == title
}.size
builder.setContentTitle(getTitle(count, title))
val inbox = NotificationCompat.InboxStyle()
.setBigContentTitle(if (singleConversation) title else summary)
.setBigContentTitle(getTitle(count, title))
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 firstMsg = pushMessageList[0]
if (firstMsg.host == null || firstMsg.message == null || firstMsg.title == null) {
return null
}
val bigText = NotificationCompat.BigTextStyle()
.bigText(firstMsg.message.fromHtml())
.setBigContentTitle(firstMsg.title.fromHtml())
builder.setStyle(bigText)
}
}
} else {
val bigText = NotificationCompat.BigTextStyle()
......@@ -250,147 +168,69 @@ object PushManager {
@SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.N)
internal fun createGroupNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification? {
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
}
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)
private fun createSingleNotification(pushMessage: PushMessage): Notification? {
with(pushMessage) {
val host = info.host
val builder = Notification.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(true)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setMessageNotification(context)
val builder = createBaseNotificationBuilder(pushMessage)
.setGroupSummary(false)
if (Build.VERSION.SDK_INT >= 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)
}
if (style == null || "inbox" == style) {
val pushMessageList = groupedPushes.hostToPushMessageList.get(host)
//TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat"
if (subText.isNotEmpty()) {
builder.setSubText(subText)
pushMessageList?.let {
val userMessages = pushMessageList.filter {
it.notificationId == pushMessage.notificationId
}
if (style == null || 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) {
if (count > 1) {
val inbox = NotificationCompat.InboxStyle()
inbox.setBigContentTitle(getTitle(count, title))
for (push in userMessages) {
inbox.addLine(push.message)
}
builder.setStyle(inbox)
}
} else {
val bigText = Notification.BigTextStyle()
val bigTextStyle = NotificationCompat.BigTextStyle()
.bigText(message.fromHtml())
.setBigContentTitle(title.fromHtml())
builder.setStyle(bigText)
}
return builder.build()
}
}
internal fun createSingleNotification(context: Context, lastPushMessage: PushMessage): Notification? {
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
}
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()
//TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat"
if (subText.isNotEmpty()) {
builder.setSubText(subText)
builder.setStyle(bigTextStyle)
}
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val lastPushMsg = pushMessageList.last()
if (lastPushMsg.host == null || lastPushMsg.message == null || lastPushMsg.title == null) {
return null
}
if (pushMessageList.isNotEmpty()) {
val messageCount = pushMessageList.size
val bigText = NotificationCompat.BigTextStyle()
.bigText(lastPushMsg.message.fromHtml())
.setBigContentTitle(lastPushMsg.title.fromHtml())
builder.setStyle(bigText).setNumber(messageCount)
}
} else {
val bigTextStyle = NotificationCompat.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
}
return builder.build()
}
}
@SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.N)
internal fun createSingleNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification? {
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createBaseNotificationBuilder(pushMessage: PushMessage, grouped: Boolean = false): NotificationCompat.Builder {
return with(pushMessage) {
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val host = info.host
val contentIntent = getContentIntent(context, id, pushMessage, grouped)
val deleteIntent = getDismissIntent(context, pushMessage)
val builder = Notification.Builder(context)
.setWhen(createdAt)
val builder = NotificationCompat.Builder(context, host)
.setWhen(info.createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(false)
.setDeleteIntent(deleteIntent)
.setContentIntent(contentIntent)
.setMessageNotification(context)
.addReplyAction(context, lastPushMessage)
.setMessageNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(host)
val channel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.enableLights(false)
......@@ -400,47 +240,18 @@ object PushManager {
}
//TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat"
val subText = getSiteName(host)
if (subText.isNotEmpty()) {
builder.setSubText(subText)
}
if (style == null || "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)
}
return@with builder
}
} else {
val bigTextStyle = Notification.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
}
return builder.build()
}
private fun getSiteName(host: String): String {
val settings = getSettingsInteractor.get(host)
return settings.siteName() ?: "Rocket.Chat"
}
private fun getTitle(messageCount: Int, title: String): CharSequence {
......@@ -450,18 +261,16 @@ object PushManager {
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)
.putExtra(EXTRA_HOSTNAME, pushMessage.info.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)
}
val notificationIntent = context.changeServerIntent(pushMessage.info.host)
// TODO - add support to go directly to the chatroom
/*if (!grouped) {
notificationIntent.putExtra(EXTRA_ROOM_ID, pushMessage.info.roomId)
}*/
return PendingIntent.getActivity(context, randomizer.nextInt(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
......@@ -472,18 +281,18 @@ object PushManager {
//Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(ctx: Context, pushMessage: PushMessage): Notification.Builder {
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(ctx, ReplyReceiver::class.java)
// val replyIntent = Intent(context, ReplyReceiver::class.java)
// replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable)
// val pendingIntent = PendingIntent.getBroadcast(
// ctx, randomizer.nextInt(), replyIntent, 0)
// context, randomizer.nextInt(), replyIntent, 0)
// val replyAction =
// Notification.Action.Builder(
// Icon.createWithResource(ctx, R.drawable.ic_reply), REPLY_LABEL, pendingIntent)
// Icon.createWithResource(context, R.drawable.ic_reply), REPLY_LABEL, pendingIntent)
// .addRemoteInput(replyRemoteInput)
// .setAllowGeneratedReplies(true)
// .build()
......@@ -491,25 +300,8 @@ object PushManager {
return this
}
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.setMessageNotification(ctx: Context): Notification.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
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.colorPrimary, ctx.theme))
setSmallIcon(smallIcon)
setSound(alarmSound)
})
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()
......@@ -529,76 +321,66 @@ object PushManager {
private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val ctx = this.mContext
val res = ctx.resources
val res = context.resources
val smallIcon = res.getIdentifier(
"rocket_chat_notification", "drawable", ctx.packageName)
"rocket_chat_notification", "drawable", context.packageName)
with(this, {
setAutoCancel(true)
setShowWhen(true)
color = ctx.resources.getColor(R.color.colorPrimary)
color = context.resources.getColor(R.color.colorPrimary)
setDefaults(Notification.DEFAULT_ALL)
setSmallIcon(smallIcon)
setSound(alarmSound)
})
return this
}
}
internal data class PushMessage(
val title: String? = null,
val message: String? = null,
data class PushMessage(
val title: String,
val message: String,
val info: PushInfo,
val image: String? = null,
val ejson: String? = null,
val count: String? = null,
val notificationId: String,
val summaryText: String? = null,
val style: String? = null) : Serializable {
val host: String?
val rid: String?
val type: String?
val channelName: String?
val sender: Sender?
val style: String? = null
)
@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?
) {
val createdAt: Long
init {
val json = if (ejson == null) JSONObject() else JSONObject(ejson)
host = json.optString("host", null)
rid = json.optString("rid", null)
type = json.optString("type", null)
channelName = json.optString("name", null)
val senderJson = json.optString("sender", null)
if (senderJson != null && senderJson != "null") {
sender = Sender(senderJson)
} else {
sender = null
}
createdAt = System.currentTimeMillis()
get() = System.currentTimeMillis()
val host by lazy {
sanitizeUrl(hostname)
}
data class Sender(val sender: String) : Serializable {
val _id: String?
val username: String?
val name: String?
init {
val json = JSONObject(sender)
_id = json.optString("_id", null)
username = json.optString("username", null)
name = json.optString("name", null)
}
}
private fun sanitizeUrl(baseUrl: String): String {
var url = baseUrl.trim()
while (url.endsWith('/')) {
url = url.dropLast(1)
}
/**
* 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)
}
}
return url
}
}
@JsonSerializable
data class PushSender(
@Json(name = "_id") val id: String,
val username: String?,
val name: String?
)
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"
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.DeleteReceiver
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class DeleteReceiverProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.GcmListenerService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class GcmListenerServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideGcmListenerService(): GcmListenerService
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get(url: String) = repository.load().firstOrNull { account ->
url == account.serverUrl
}
}
\ No newline at end of file
package chat.rocket.android.server.domain;
import java.util.List;
import io.reactivex.Scheduler;
import io.reactivex.Single;
public class GetServersInteractor {
private final ServersRepository repository;
private final Scheduler executionScheduler;
public GetServersInteractor(ServersRepository repository, Scheduler executionScheduler) {
this.repository = repository;
this.executionScheduler = executionScheduler;
}
}
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Server
import chat.rocket.android.server.infraestructure.ServerEntity
import io.reactivex.Completable
import io.reactivex.Single
interface ServersRepository {
val servers: Single<List<Server>>
fun saveServer(server: Server): Completable
fun updateServer(server: Server): Completable
}
\ No newline at end of file
......@@ -96,3 +96,4 @@ fun PublicSettings.uploadMaxFileSize(): Int {
}
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
package chat.rocket.android.server.domain
interface TokenRepository : chat.rocket.core.TokenRepository {
fun remove(url: String)
fun clear()
}
\ No newline at end of file
......@@ -2,16 +2,16 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient,
val repository: TokenRepository,
val logger: PlatformLogger) {
class RocketChatClientFactory @Inject constructor(private val okHttpClient: OkHttpClient,
private val repository: TokenRepository,
private val logger: PlatformLogger) {
private val cache = HashMap<String, RocketChatClient>()
fun create(url: String): RocketChatClient {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.ServersRepository
import chat.rocket.android.server.domain.model.Server
import io.reactivex.Completable
import io.reactivex.Single
class RoomServersRepository : ServersRepository {
override val servers: Single<List<Server>>
get() = TODO("not implemented")
override fun saveServer(server: Server): Completable {
TODO("not implemented")
}
override fun updateServer(server: Server): Completable {
TODO("not implemented")
}
}
package chat.rocket.android.server.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.TokenRepository
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(private val view: ChangeServerView,
class ChangeServerPresenter @Inject constructor(
private val view: ChangeServerView,
private val navigator: ChangeServerNavigator,
private val strategy: CancelStrategy,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val multiServerRepository: MultiServerTokenRepository,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository,
private val connectionManager: ConnectionManagerFactory) {
private val localRepository: LocalRepository,
private val connectionManager: ConnectionManagerFactory
) {
fun loadServer(newUrl: String?) {
launchUI(strategy) {
view.showProgress()
......@@ -29,7 +31,7 @@ class ChangeServerPresenter @Inject constructor(private val view: ChangeServerVi
}
url?.let { serverUrl ->
val token = multiServerRepository.get(serverUrl)
val token = tokenRepository.get(serverUrl)
if (token == null) {
view.showInvalidCredentials()
view.hideProgress()
......@@ -47,7 +49,11 @@ class ChangeServerPresenter @Inject constructor(private val view: ChangeServerVi
connectionManager.get(url)?.disconnect()
}
tokenRepository.save(Token(token.userId, token.authToken))
// Save the current username.
getAccountInteractor.get(serverUrl)?.let { account ->
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
saveCurrentServerInteractor.save(serverUrl)
view.hideProgress()
navigator.toChatRooms()
......
......@@ -39,7 +39,7 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
private const val INTENT_SERVER_URL = "INTENT_SERVER_URL"
private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
fun Context.changeServerIntent(serverUrl: String?): Intent {
return Intent(this, ChangeServerActivity::class.java).apply {
......
package chat.rocket.android.util.extensions
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
suspend fun RocketChatClient.registerPushToken(
token: String,
accounts: List<Account>,
factory: RocketChatClientFactory
) {
launch(CommonPool) {
accounts.forEach { account ->
try {
factory.create(account.serverUrl).registerPushToken(token)
} catch (ex: Exception) {
Timber.d(ex, "Error registering Push token for ${account.serverUrl}")
ex.printStackTrace()
}
}
}
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ ext {
threeTenABP : '1.0.5',
rxBinding : '2.0.0',
fresco : '1.8.1',
kotshi : '0.3.0',
kotshi : '1.0.2',
frescoImageViewer : '0.5.1',
markwon : '1.0.3',
sheetMenu : '1.3.3',
......
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