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

Merge branch 'beta' into new/read-receipts

parents 034d91bf d2468b7b
......@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2024
versionName "2.3.0"
versionCode 2026
versionName "2.3.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......
......@@ -16,13 +16,22 @@ import chat.rocket.android.app.migration.model.RealmBasedServerInfo
import chat.rocket.android.app.migration.model.RealmPublicSetting
import chat.rocket.android.app.migration.model.RealmSession
import chat.rocket.android.app.migration.model.RealmUser
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.infrastructure.installCrashlyticsWrapper
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.widget.emoji.EmojiRepository
......@@ -34,13 +43,16 @@ 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.*
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
......@@ -69,7 +81,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject
lateinit var multiServerRepository: MultiServerTokenRepository
lateinit var settingsInteractor: GetSettingsInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
......@@ -81,8 +93,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var prefs: SharedPreferences
@Inject
lateinit var getAccountsInteractor: GetAccountsInteractor
@Inject
lateinit var localRepository: LocalRepository
@Inject
......@@ -101,8 +111,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
.lifecycle
.addObserver(appLifecycleObserver)
// TODO - remove this on the future, temporary migration stuff for pre-release versions.
migrateInternalTokens()
context = WeakReference(applicationContext)
AndroidThreeTen.init(this)
......@@ -127,6 +135,29 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
} catch (ex: Exception) {
Timber.d(ex, "Error migrating old accounts")
}
// TODO - remove this
checkCurrentServer()
}
private fun checkCurrentServer() {
val currentServer = getCurrentServerInteractor.get() ?: "<unknown>"
if (currentServer == "<unknown>") {
val message = "null currentServer"
Timber.d(IllegalStateException(message), message)
}
val settings = settingsInteractor.get(currentServer)
if (settings.isEmpty()) {
val message = "Empty settings for: $currentServer"
Timber.d(IllegalStateException(message), message)
}
val baseUrl = settings[SITE_URL]
if (baseUrl == null) {
val message = "Server $currentServer SITE_URL"
Timber.d(IllegalStateException(message), message)
}
}
private fun migrateFromLegacy() {
......@@ -233,32 +264,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
}
}
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() {
val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()
Fabric.with(this, Crashlytics.Builder().core(core).build())
installCrashlyticsWrapper(this@RocketChatApplication,
getCurrentServerInteractor, settingsInteractor,
accountRepository, localRepository)
}
private fun setupFresco() {
......@@ -301,6 +313,4 @@ private fun LocalRepository.hasMigrated() = getBoolean(LocalRepository.MIGRATION
private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true)
private fun LocalRepository.setOldMessagesCleanedUp() = save(CLEANUP_OLD_MESSAGES_NEEDED, false)
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.di
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.qualifier.ForAuthentication
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
......
......@@ -41,12 +41,13 @@ class LoginPresenter @Inject constructor(
private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor,
private val settingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
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 var currentServer = serverInteractor.get()!!
private lateinit var client: RocketChatClient
private lateinit var settings: PublicSettings
private lateinit var usernameOrEmail: String
......@@ -103,6 +104,7 @@ class LoginPresenter @Inject constructor(
}
private fun setupConnectionInfo(serverUrl: String) {
currentServer = serverUrl
client = factory.create(serverUrl)
settings = settingsInteractor.get(serverUrl)
}
......@@ -325,6 +327,7 @@ class LoginPresenter @Inject constructor(
val username = retryIO("me()") { client.me().username }
if (username != null) {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveCurrentServer.save(currentServer)
saveAccount(username)
saveToken(token)
registerPushToken()
......
......@@ -27,7 +27,8 @@ class RegisterUsernamePresenter @Inject constructor(
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
serverInteractor: GetCurrentServerInteractor,
serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
......@@ -47,6 +48,7 @@ class RegisterUsernamePresenter @Inject constructor(
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
saveCurrentServer.save(currentServer)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
navigator.toChatList()
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail
......@@ -19,7 +20,7 @@ class ResetPasswordPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
factory: RocketChatClientFactory,
serverInteractor: GetCurrentServerInteractor
serverInteractor: GetConnectingServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
......
......@@ -6,7 +6,7 @@ import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl
......@@ -16,7 +16,7 @@ import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory
......
......@@ -22,7 +22,8 @@ class SignupPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
......@@ -60,6 +61,7 @@ class SignupPresenter @Inject constructor(
// TODO This function returns a user token so should we save it?
retryIO("login") { client.login(username, password) }
val me = retryIO("me") { client.me() }
saveCurrentServerInteractor.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
......
......@@ -25,7 +25,8 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
......@@ -55,6 +56,7 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
}
val me = retryIO("me") { client.me() }
saveAccount(me)
saveCurrentServerInteractor.save(currentServer)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
......
......@@ -32,17 +32,25 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
// if we got authenticateWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState, deepLinkInfo)
showServerInput(deepLinkInfo)
}
}
}
}
override fun onStop() {
job.cancel()
super.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......@@ -53,17 +61,12 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) {
fun showServerInput(deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container, allowStateLoss = true) {
ServerFragment.newInstance(deepLinkInfo)
}
}
......
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
......@@ -12,20 +10,22 @@ import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class ChatRoomFragmentModule {
@Provides
@PerFragment
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: ChatRoomFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
......
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -8,5 +9,6 @@ import dagger.android.ContributesAndroidInjector
abstract class ChatRoomFragmentProvider {
@ContributesAndroidInjector(modules = [ChatRoomFragmentModule::class])
@PerFragment
abstract fun provideChatRoomFragment(): ChatRoomFragment
}
\ No newline at end of file
......@@ -813,7 +813,7 @@ class ChatRoomPresenter @Inject constructor(
}
}
if (typingStatusList.isNotEmpty()) {
view.showTypingStatus(typingStatusList)
view.showTypingStatus(typingStatusList.toList()) // copy typingStatusList
} else {
view.hideTypingStatusView()
}
......
......@@ -31,7 +31,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param usernameList The list of username to show.
*/
fun showTypingStatus(usernameList: ArrayList<String>)
fun showTypingStatus(usernameList: List<String>)
/**
* Hides the typing status view.
......
......@@ -376,7 +376,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun showTypingStatus(usernameList: ArrayList<String>) {
override fun showTypingStatus(usernameList: List<String>) {
ui {
when (usernameList.size) {
1 -> {
......
......@@ -15,6 +15,7 @@ import androidx.core.text.scale
import chat.rocket.android.R
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UserHelper
......@@ -53,6 +54,7 @@ import okhttp3.HttpUrl
import java.security.InvalidParameterException
import javax.inject.Inject
@PerFragment
class ViewModelMapper @Inject constructor(
private val context: Context,
private val parser: MessageParser,
......@@ -67,8 +69,8 @@ class ViewModelMapper @Inject constructor(
) {
private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl()
private val settings = getSettingsInteractor.get(currentServer)
private val baseUrl = currentServer
private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
......
package chat.rocket.android.chatrooms.di
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -8,5 +9,6 @@ import dagger.android.ContributesAndroidInjector
abstract class ChatRoomsFragmentProvider {
@ContributesAndroidInjector(modules = [ChatRoomsFragmentModule::class])
@PerFragment
abstract fun provideChatRoomsFragment(): ChatRoomsFragment
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog
import android.os.Bundle
import android.os.Handler
import android.renderscript.RSInvalidStateException
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.util.DiffUtil
......@@ -37,11 +38,13 @@ import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import com.crashlytics.android.Crashlytics
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.NonCancellable.isActive
import timber.log.Timber
import java.security.InvalidParameterException
import javax.inject.Inject
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
......
......@@ -14,6 +14,7 @@ import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForAuthentication
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
......@@ -23,6 +24,7 @@ import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.HttpLoggingInterceptor
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
......@@ -42,7 +44,6 @@ import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
......@@ -54,7 +55,9 @@ class AppModule {
@Provides
@Singleton
fun provideRocketChatClient(okHttpClient: OkHttpClient, repository: TokenRepository, logger: PlatformLogger): RocketChatClient {
fun provideRocketChatClient(okHttpClient: OkHttpClient,
repository: TokenRepository,
logger: PlatformLogger): RocketChatClient {
return RocketChatClient.create {
httpClient = okHttpClient
tokenRepository = repository
......@@ -68,7 +71,8 @@ class AppModule {
@Provides
@Singleton
fun provideRocketChatDatabase(context: Application): RocketChatDatabase {
return Room.databaseBuilder(context.applicationContext, RocketChatDatabase::class.java, "rocketchat-db").build()
return Room.databaseBuilder(context.applicationContext, RocketChatDatabase::class.java,
"rocketchat-db").build()
}
@Provides
......@@ -91,8 +95,10 @@ class AppModule {
@Provides
@Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
val interceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
})
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
......@@ -168,6 +174,12 @@ class AppModule {
return SharedPrefsCurrentServerRepository(prefs)
}
@Provides
@ForAuthentication
fun provideConnectingServerRepository(prefs: SharedPreferences): CurrentServerRepository {
return SharedPrefsConnectingServerRepository(prefs)
}
@Provides
@Singleton
fun provideSettingsRepository(localRepository: LocalRepository): SettingsRepository {
......
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForAuthentication
\ No newline at end of file
......@@ -2,10 +2,6 @@ package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
/**
* Created by luciofm on 4/14/18.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForMessages
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -8,5 +9,6 @@ import dagger.android.ContributesAndroidInjector
abstract class FavoriteMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [FavoriteMessagesFragmentModule::class])
@PerFragment
abstract fun provideFavoriteMessageFragment(): FavoriteMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.infrastructure
import android.app.Application
import chat.rocket.android.BuildConfig
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.SITE_URL
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.experimental.runBlocking
fun installCrashlyticsWrapper(context: Application,
currentServerInteractor: GetCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor,
accountRepository: AccountsRepository,
localRepository: LocalRepository) {
if (isCrashlyticsEnabled()) {
Thread.setDefaultUncaughtExceptionHandler(RocketChatUncaughtExceptionHandler(currentServerInteractor,
settingsInteractor, accountRepository, localRepository))
}
}
private fun isCrashlyticsEnabled(): Boolean {
return !BuildConfig.DEBUG
}
private class RocketChatUncaughtExceptionHandler(
val currentServerInteractor: GetCurrentServerInteractor,
val settingsInteractor: GetSettingsInteractor,
val accountRepository: AccountsRepository,
val localRepository: LocalRepository)
: Thread.UncaughtExceptionHandler {
val crashlyticsHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(t: Thread, e: Throwable) {
val currentServer = currentServerInteractor.get() ?: "<unknown>"
Crashlytics.setString(KEY_CURRENT_SERVER, currentServer)
runBlocking {
val accounts = accountRepository.load()
Crashlytics.setString(KEY_ACCOUNTS, accounts.toString())
}
val settings = settingsInteractor.get(currentServer)
Crashlytics.setInt(KEY_SETTINGS_SIZE, settings.size)
val baseUrl = settings[SITE_URL]?.toString()
Crashlytics.setString(KEY_SETTINGS_BASE_URL, baseUrl)
val user = localRepository.getCurrentUser(currentServer)
Crashlytics.setString(KEY_CURRENT_USER, user?.toString())
Crashlytics.setString(KEY_CURRENT_USERNAME, localRepository.username())
if (crashlyticsHandler != null) {
crashlyticsHandler.uncaughtException(t, e)
} else {
throw RuntimeException("Missing default exception handler")
}
}
}
private const val KEY_CURRENT_SERVER = "CURRENT_SERVER"
private const val KEY_CURRENT_USER = "CURRENT_USER"
private const val KEY_CURRENT_USERNAME = "CURRENT_USERNAME"
private const val KEY_ACCOUNTS = "ACCOUNTS"
private const val KEY_SETTINGS_SIZE = "SETTINGS_SIZE"
private const val KEY_SETTINGS_BASE_URL = "SETTINGS_BASE_URL"
\ No newline at end of file
package chat.rocket.android.pinnedmessages.di
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentModule
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.pinnedmessages.ui.PinnedMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -9,5 +10,6 @@ import dagger.android.ContributesAndroidInjector
abstract class PinnedMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentModule::class])
@PerFragment
abstract fun providePinnedMessageFragment(): PinnedMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.dagger.qualifier.ForAuthentication
import javax.inject.Inject
class GetConnectingServerInteractor @Inject constructor(
@ForAuthentication private val repository: CurrentServerRepository
) {
fun get(): String? = repository.get()
fun clear() {
repository.clear()
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.dagger.qualifier.ForAuthentication
import javax.inject.Inject
class SaveConnectingServerInteractor @Inject constructor(
@ForAuthentication private val repository: CurrentServerRepository
) {
fun save(url: String) = repository.save(url)
}
\ No newline at end of file
......@@ -5,11 +5,6 @@ import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings
}
// Authentication methods.
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
......@@ -110,3 +105,8 @@ fun PublicSettings.uploadMaxFileSize(): Int {
fun PublicSettings.baseUrl(): String = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
interface SettingsRepository {
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import chat.rocket.android.infrastructure.LocalRepository.Companion.SETTINGS_KEY
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter
import timber.log.Timber
class SharedPreferencesSettingsRepository(
private val localRepository: LocalRepository
......@@ -13,11 +14,27 @@ class SharedPreferencesSettingsRepository(
private val adapter = SettingsAdapter().lenient()
override fun save(url: String, settings: PublicSettings) {
if (settings.isEmpty()) {
val message = "Saving empty settings for $SETTINGS_KEY$url"
Timber.d(IllegalStateException(message), message)
}
localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
}
override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url")
return if (settings == null) hashMapOf() else adapter.fromJson(settings) ?: hashMapOf()
val settingsStr = localRepository.get("$SETTINGS_KEY$url")
return if (settingsStr == null) {
val message = "NULL Settings for: $SETTINGS_KEY$url"
Timber.d(IllegalStateException(message), message)
hashMapOf()
} else {
val settings = adapter.fromJson(settingsStr)
if (settings == null) {
val message = "NULL Settings for: $SETTINGS_KEY$url with saved settings: $settingsStr"
Timber.d(IllegalStateException(message), message)
}
settings ?: hashMapOf()
}
}
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import chat.rocket.android.server.domain.CurrentServerRepository
class SharedPrefsConnectingServerRepository(private val preferences: SharedPreferences) : CurrentServerRepository {
override fun save(url: String) {
preferences.edit().putString(CONNECTING_SERVER_KEY, url).apply()
}
override fun get(): String? {
return preferences.getString(CONNECTING_SERVER_KEY, null)
}
companion object {
private const val CONNECTING_SERVER_KEY = "connecting_server"
}
override fun clear() {
preferences.edit().remove(CONNECTING_SERVER_KEY).apply()
}
}
\ No newline at end of file
package chat.rocket.android.util
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.http.HttpHeaders
import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.INFO
import okio.Buffer
import okio.GzipSource
import java.io.EOFException
import java.io.IOException
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor which logs request and response information. Can be applied as an
* [application interceptor][OkHttpClient.interceptors] or as a [ ][OkHttpClient.networkInterceptors].
*
* The format of the logs created by
* this class should not be considered stable and may change slightly between releases. If you need
* a stable logging format, use your own interceptor.
*/
class HttpLoggingInterceptor constructor(private val logger: Logger) : Interceptor {
@Volatile
internal var level = Level.NONE
enum class Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
*
* Example:
* <pre>`--> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
`</pre> *
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
*
*
* Example:
* <pre>`--> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
`</pre> *
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
*
* Example:
* <pre>`--> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
`</pre> *
*/
BODY
}
interface Logger {
fun log(message: String)
}
/** Change the level at which this interceptor logs. */
fun setLevel(level: Level): HttpLoggingInterceptor {
this.level = level
return this
}
fun getLevel(): Level {
return level
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val level = this.level
val request = chain.request()
if (level == Level.NONE) {
return chain.proceed(request)
}
val logBody = level == Level.BODY
val logHeaders = logBody || level == Level.HEADERS
val requestBody = request.body()
val hasRequestBody = requestBody != null
val connection = chain.connection()
var requestStartMessage = ("--> ${request.method()} ${request.url()}"
+ if (connection != null) " " + connection.protocol() else "")
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (${requestBody!!.contentLength()}-byte body)"
}
logger.log(requestStartMessage)
if (logHeaders) {
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody!!.contentType() != null) {
logger.log("Content-Type: ${requestBody.contentType()!!}")
}
if (requestBody.contentLength() != -1L) {
logger.log("Content-Length: ${requestBody.contentLength()}")
}
}
val headers = request.headers()
var i = 0
val count = headers.size()
while (i < count) {
val name = headers.name(i)
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(name, ignoreCase = true)) {
if ("X-Auth-Token".equals(name, ignoreCase = true)) {
logger.log("$name: ${skipAuthToken(headers.value(i).length)}")
} else {
logger.log("$name: ${headers.value(i)}")
}
}
i++
}
if (!logBody || !hasRequestBody) {
logger.log("--> END ${request.method()}")
} else if (bodyHasUnknownEncoding(request.headers())) {
logger.log("--> END ${request.method()} (encoded body omitted)")
} else if (isMultipart(requestBody?.contentType())) {//requestBody?.contentType()?.toString()?.contains("multipart/form-data", ignoreCase = true)) {
logger.log("--> END ${request.method()} (multipart body omitted)")
} else if (hasRequestBody) {
val buffer = Buffer()
requestBody!!.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
logger.log("")
if (isPlaintext(buffer)) {
logger.log(buffer.readString(charset!!))
logger.log("--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)")
} else {
logger.log("--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)")
}
}
}
val startNs = System.nanoTime()
val response: Response
try {
response = chain.proceed(request)
} catch (e: Exception) {
logger.log("<-- HTTP FAILED: $e")
throw e
}
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val responseBody = response.body()
val contentLength = responseBody!!.contentLength()
val bodySize = if (contentLength != -1L) contentLength.toString() + "-byte" else "unknown-length"
val responseStr = if (response.message().isEmpty()) "" else " ${response.message()}"
logger.log("<-- ${response.code()}$responseStr ${response.request().url()}"
+ " (" + tookMs + "ms" + (if (!logHeaders) ", $bodySize body" else "") + ')'.toString())
if (logHeaders) {
val headers = response.headers()
var i = 0
val count = headers.size()
while (i < count) {
logger.log(headers.name(i) + ": " + headers.value(i))
i++
}
if (!logBody || !HttpHeaders.hasBody(response)) {
logger.log("<-- END HTTP")
} else if (bodyHasUnknownEncoding(response.headers())) {
logger.log("<-- END HTTP (encoded body omitted)")
} else {
val source = responseBody.source()
source.request(java.lang.Long.MAX_VALUE) // Buffer the entire body.
var buffer = source.buffer()
var gzippedLength: Long? = null
if (headers.get("Content-Encoding")?.equals("gzip", ignoreCase = true) == true) {
gzippedLength = buffer.size()
var gzippedResponseBody: GzipSource? = null
try {
gzippedResponseBody = GzipSource(buffer.clone())
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
} finally {
if (gzippedResponseBody != null) {
gzippedResponseBody.close()
}
}
}
var charset: Charset? = UTF8
val contentType = responseBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
if (!isPlaintext(buffer)) {
logger.log("")
logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)")
return response
}
if (contentLength != 0L) {
logger.log("")
logger.log(buffer.clone().readString(charset!!))
}
if (gzippedLength != null) {
logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)")
} else {
logger.log("<-- END HTTP (" + buffer.size() + "-byte body)")
}
}
}
return response
}
private fun isMultipart(contentType: MediaType?): Boolean {
return contentType?.let {
contentType.toString().contains("multipart/form-data")
} ?: false
}
private fun skipAuthToken(length: Int): String {
val builder = StringBuilder(length)
for (i in 1..length) builder.append("X")
return builder.toString()
}
private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
val contentEncoding = headers.get("Content-Encoding")
return (contentEncoding != null
&& !contentEncoding.equals("identity", ignoreCase = true)
&& !contentEncoding.equals("gzip", ignoreCase = true))
}
companion object {
private val UTF8 = Charset.forName("UTF-8")
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
internal fun isPlaintext(buffer: Buffer): Boolean {
try {
val prefix = Buffer()
val byteCount = if (buffer.size() < 64) buffer.size() else 64
buffer.copyTo(prefix, 0, byteCount)
for (i in 0..15) {
if (prefix.exhausted()) {
break
}
val codePoint = prefix.readUtf8CodePoint()
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false
}
}
return true
} catch (e: EOFException) {
return false // Truncated UTF-8 sequence.
}
}
}
}
......@@ -31,11 +31,16 @@ fun View.isVisible(): Boolean {
fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(resource, this, attachToRoot)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, allowStateLoss: Boolean = false,
newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction()
val transaction = supportFragmentManager.beginTransaction()
.replace(layoutId, fragment, tag)
.commit()
if (allowStateLoss) {
transaction.commitAllowingStateLoss()
} else {
transaction.commit()
}
}
fun AppCompatActivity.addFragmentBackStack(
......
......@@ -5,12 +5,17 @@ import android.support.customtabs.CustomTabsIntent
import android.support.v4.content.res.ResourcesCompat
import android.view.View
import chat.rocket.android.R
import timber.log.Timber
fun View.openTabbedUrl(url: Uri) {
with(this) {
val tabsbuilder = CustomTabsIntent.Builder()
tabsbuilder.setToolbarColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
val customTabsIntent = tabsbuilder.build()
try {
customTabsIntent.launchUrl(context, url)
} catch (ex: Exception) {
Timber.d(ex, "Unable to launch URL")
}
}
}
\ 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