Commit bb20f4bc authored by Lucio Maciel's avatar Lucio Maciel

Merge branch 'develop' into feature/db-chatrooms

parents dbee09cd 4d125818
...@@ -12,7 +12,7 @@ android { ...@@ -12,7 +12,7 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2023 versionCode 2030
versionName "2.4.0" versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
...@@ -25,12 +25,19 @@ android { ...@@ -25,12 +25,19 @@ android {
keyAlias System.getenv("KEY_ALIAS") keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD") keyPassword System.getenv("KEY_PASSWORD")
} }
debug {
storeFile project.rootProject.file('debug.keystore').getCanonicalFile()
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
}
} }
buildTypes { buildTypes {
release { release {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"' buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"' buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.64.2"'
signingConfig signingConfigs.release signingConfig signingConfigs.release
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
...@@ -38,7 +45,8 @@ android { ...@@ -38,7 +45,8 @@ android {
debug { debug {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"' buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"' buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.64.2"'
signingConfig signingConfigs.debug
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
} }
} }
...@@ -72,7 +80,7 @@ dependencies { ...@@ -72,7 +80,7 @@ dependencies {
kapt libraries.daggerProcessor kapt libraries.daggerProcessor
kapt libraries.daggerAndroidApt kapt libraries.daggerAndroidApt
implementation libraries.playServicesGcm implementation libraries.fcm
implementation libraries.playServicesAuth implementation libraries.playServicesAuth
implementation libraries.room implementation libraries.room
......
This diff is collapsed.
...@@ -3,17 +3,9 @@ ...@@ -3,17 +3,9 @@
package="chat.rocket.android"> package="chat.rocket.android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
android:allowBackup="true" android:allowBackup="true"
...@@ -23,18 +15,20 @@ ...@@ -23,18 +15,20 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
android:name=".authentication.ui.AuthenticationActivity" android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation" android:configChanges="orientation"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AuthenticationTheme" android:theme="@style/AuthenticationTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
...@@ -50,25 +44,31 @@ ...@@ -50,25 +44,31 @@
android:scheme="https" /> android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".server.ui.ChangeServerActivity" android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" /> android:theme="@style/AuthenticationTheme" />
<activity <activity
android:name=".main.ui.MainActivity" android:name=".main.ui.MainActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity <activity
android:name=".webview.ui.WebViewActivity" android:name=".webview.ui.WebViewActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity <activity
android:name=".webview.cas.ui.CasWebViewActivity" android:name=".webview.sso.ui.SsoWebViewActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity <activity
android:name=".webview.oauth.ui.OauthWebViewActivity" android:name=".webview.oauth.ui.OauthWebViewActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
...@@ -84,17 +84,6 @@ ...@@ -84,17 +84,6 @@
android:name=".settings.about.ui.AboutActivity" android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".push.DirectReplyReceiver" android:name=".push.DirectReplyReceiver"
android:enabled="true" android:enabled="true"
...@@ -111,13 +100,16 @@ ...@@ -111,13 +100,16 @@
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" /> <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".push.GcmListenerService" android:name=".push.FirebaseMessagingService"
android:exported="false"> android:enabled="true"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".chatroom.service.MessageService" android:name=".chatroom.service.MessageService"
android:exported="true" android:exported="true"
......
...@@ -14,7 +14,7 @@ object DateTimeHelper { ...@@ -14,7 +14,7 @@ object DateTimeHelper {
/** /**
* Returns a [LocalDateTime] from a [Long]. * Returns a [LocalDateTime] from a [Long].
* *
* @param long The [Long] * @param long The [Long] to gets a [LocalDateTime].
* @return The [LocalDateTime] from a [Long]. * @return The [LocalDateTime] from a [Long].
*/ */
fun getLocalDateTime(long: Long): LocalDateTime { fun getLocalDateTime(long: Long): LocalDateTime {
......
package chat.rocket.android.app package chat.rocket.android.app
/*import chat.rocket.android.app.migration.RealmMigration
import chat.rocket.android.app.migration.RocketChatLibraryModule
import chat.rocket.android.app.migration.RocketChatServerModule
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 android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.app.Service import android.app.Service
...@@ -16,18 +9,17 @@ import android.content.SharedPreferences ...@@ -16,18 +9,17 @@ import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.dagger.qualifier.ForMessages import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.CrashlyticsTree import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountsInteractor 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.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.TokenRepository import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.widget.emoji.EmojiRepository import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore import com.crashlytics.android.core.CrashlyticsCore
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
...@@ -40,7 +32,6 @@ import dagger.android.HasActivityInjector ...@@ -40,7 +32,6 @@ import dagger.android.HasActivityInjector
import dagger.android.HasBroadcastReceiverInjector import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasServiceInjector import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.experimental.runBlocking
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
...@@ -69,17 +60,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -69,17 +60,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject @Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject @Inject
lateinit var multiServerRepository: MultiServerTokenRepository lateinit var settingsInteractor: GetSettingsInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject @Inject
lateinit var tokenRepository: TokenRepository lateinit var tokenRepository: TokenRepository
@Inject @Inject
lateinit var prefs: SharedPreferences
@Inject
lateinit var getAccountsInteractor: GetAccountsInteractor
@Inject
lateinit var localRepository: LocalRepository lateinit var localRepository: LocalRepository
@Inject
lateinit var accountRepository: AccountsRepository
@Inject @Inject
@field:ForMessages @field:ForMessages
...@@ -97,8 +84,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -97,8 +84,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
.lifecycle .lifecycle
.addObserver(appLifecycleObserver) .addObserver(appLifecycleObserver)
// TODO - remove this on the future, temporary migration stuff for pre-release versions.
migrateInternalTokens()
context = WeakReference(applicationContext) context = WeakReference(applicationContext)
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
...@@ -116,34 +101,37 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -116,34 +101,37 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
} }
// TODO - remove REALM files. // TODO - remove REALM files.
// TODO - remove this
checkCurrentServer()
} }
private fun migrateInternalTokens() { private fun checkCurrentServer() {
if (!prefs.getBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, true)) { val currentServer = getCurrentServerInteractor.get() ?: "<unknown>"
Timber.d("Tokens already migrated")
return
}
getCurrentServerInteractor.get()?.let { serverUrl -> if (currentServer == "<unknown>") {
multiServerRepository.get(serverUrl)?.let { token -> val message = "null currentServer"
tokenRepository.save(serverUrl, Token(token.userId, token.authToken)) Timber.d(IllegalStateException(message), message)
}
} }
runBlocking { val settings = settingsInteractor.get(currentServer)
getAccountsInteractor.get().forEach { account -> if (settings.isEmpty()) {
multiServerRepository.get(account.serverUrl)?.let { token -> val message = "Empty settings for: $currentServer"
tokenRepository.save(account.serverUrl, token.toToken()) Timber.d(IllegalStateException(message), message)
} }
} val baseUrl = settings[SITE_URL]
if (baseUrl == null) {
val message = "Server $currentServer SITE_URL"
Timber.d(IllegalStateException(message), message)
} }
prefs.edit { putBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, false) }
} }
private fun setupCrashlytics() { private fun setupCrashlytics() {
val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build() val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()
Fabric.with(this, Crashlytics.Builder().core(core).build()) Fabric.with(this, Crashlytics.Builder().core(core).build())
installCrashlyticsWrapper(this@RocketChatApplication,
getCurrentServerInteractor, settingsInteractor,
accountRepository, localRepository)
} }
private fun setupFresco() { private fun setupFresco() {
...@@ -181,6 +169,4 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -181,6 +169,4 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true) private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true)
private fun LocalRepository.setOldMessagesCleanedUp() = save(CLEANUP_OLD_MESSAGES_NEEDED, false) 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" 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 ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.di
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.qualifier.ForAuthentication
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
......
...@@ -25,8 +25,9 @@ import javax.inject.Inject ...@@ -25,8 +25,9 @@ import javax.inject.Inject
private const val TYPE_LOGIN_USER_EMAIL = 0 private const val TYPE_LOGIN_USER_EMAIL = 0
private const val TYPE_LOGIN_CAS = 1 private const val TYPE_LOGIN_CAS = 1
private const val TYPE_LOGIN_OAUTH = 2 private const val TYPE_LOGIN_SAML = 2
private const val TYPE_LOGIN_DEEP_LINK = 3 private const val TYPE_LOGIN_OAUTH = 3
private const val TYPE_LOGIN_DEEP_LINK = 4
private const val SERVICE_NAME_FACEBOOK = "facebook" private const val SERVICE_NAME_FACEBOOK = "facebook"
private const val SERVICE_NAME_GITHUB = "github" private const val SERVICE_NAME_GITHUB = "github"
private const val SERVICE_NAME_GOOGLE = "google" private const val SERVICE_NAME_GOOGLE = "google"
...@@ -41,12 +42,13 @@ class LoginPresenter @Inject constructor( ...@@ -41,12 +42,13 @@ class LoginPresenter @Inject constructor(
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val settingsInteractor: GetSettingsInteractor, private val settingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val factory: RocketChatClientFactory private val factory: RocketChatClientFactory
) { ) {
// TODO - we should validate the current server when opening the app, and have a nonnull get() // 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 client: RocketChatClient
private lateinit var settings: PublicSettings private lateinit var settings: PublicSettings
private lateinit var usernameOrEmail: String private lateinit var usernameOrEmail: String
...@@ -55,7 +57,6 @@ class LoginPresenter @Inject constructor( ...@@ -55,7 +57,6 @@ class LoginPresenter @Inject constructor(
private lateinit var credentialSecret: String private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String private lateinit var deepLinkToken: String
private var loginCredentials: Credential? = null
fun setupView() { fun setupView() {
setupConnectionInfo(currentServer) setupConnectionInfo(currentServer)
...@@ -82,14 +83,19 @@ class LoginPresenter @Inject constructor( ...@@ -82,14 +83,19 @@ class LoginPresenter @Inject constructor(
} }
} }
fun authenticateWithCas(token: String) { fun authenticateWithCas(casToken: String) {
credentialToken = token credentialToken = casToken
doAuthentication(TYPE_LOGIN_CAS) doAuthentication(TYPE_LOGIN_CAS)
} }
fun authenticateWithOauth(token: String, secret: String) { fun authenticateWithSaml(samlToken: String) {
credentialToken = token credentialToken = samlToken
credentialSecret = secret doAuthentication(TYPE_LOGIN_SAML)
}
fun authenticateWithOauth(oauthToken: String, oauthSecret: String) {
credentialToken = oauthToken
credentialSecret = oauthSecret
doAuthentication(TYPE_LOGIN_OAUTH) doAuthentication(TYPE_LOGIN_OAUTH)
} }
...@@ -99,11 +105,11 @@ class LoginPresenter @Inject constructor( ...@@ -99,11 +105,11 @@ class LoginPresenter @Inject constructor(
deepLinkUserId = deepLinkInfo.userId deepLinkUserId = deepLinkInfo.userId
deepLinkToken = deepLinkInfo.token deepLinkToken = deepLinkInfo.token
tokenRepository.save(serverUrl, Token(deepLinkUserId, deepLinkToken)) tokenRepository.save(serverUrl, Token(deepLinkUserId, deepLinkToken))
doAuthentication(TYPE_LOGIN_DEEP_LINK) doAuthentication(TYPE_LOGIN_DEEP_LINK)
} }
private fun setupConnectionInfo(serverUrl: String) { private fun setupConnectionInfo(serverUrl: String) {
currentServer = serverUrl
client = factory.create(serverUrl) client = factory.create(serverUrl)
settings = settingsInteractor.get(serverUrl) settings = settingsInteractor.get(serverUrl)
} }
...@@ -124,8 +130,11 @@ class LoginPresenter @Inject constructor( ...@@ -124,8 +130,11 @@ class LoginPresenter @Inject constructor(
private fun setupCasView() { private fun setupCasView() {
if (settings.isCasAuthenticationEnabled()) { if (settings.isCasAuthenticationEnabled()) {
val token = generateRandomString(17) val casToken = generateRandomString(17)
view.setupCasButtonListener(settings.casLoginUrl().casUrl(currentServer, token), token) view.setupCasButtonListener(
settings.casLoginUrl().casUrl(currentServer, casToken),
casToken
)
view.showCasButton() view.showCasButton()
} }
} }
...@@ -216,7 +225,7 @@ class LoginPresenter @Inject constructor( ...@@ -216,7 +225,7 @@ class LoginPresenter @Inject constructor(
// totalSocialAccountsEnabled++ // totalSocialAccountsEnabled++
} }
if (settings.isTwitterAuthenticationEnabled()) { if (settings.isTwitterAuthenticationEnabled()) {
//TODO: Remove until we have this implemented //TODO: Remove until Twitter provides support to OAuth2
// view.enableLoginByTwitter() // view.enableLoginByTwitter()
// totalSocialAccountsEnabled++ // totalSocialAccountsEnabled++
} }
...@@ -261,8 +270,23 @@ class LoginPresenter @Inject constructor( ...@@ -261,8 +270,23 @@ class LoginPresenter @Inject constructor(
customOauthUrl, customOauthUrl,
state, state,
serviceName, serviceName,
getCustomOauthServiceNameColor(service), getServiceNameColor(service),
getCustomOauthButtonColor(service) getServiceButtonColor(service)
)
totalSocialAccountsEnabled++
}
}
getSamlServices(services).let {
val samlToken = generateRandomString(17)
for (service in it) {
view.addSamlServiceButton(
currentServer.samlUrl(getSamlProvider(service), samlToken),
samlToken,
getSamlServiceName(service),
getServiceNameColor(service),
getServiceButtonColor(service)
) )
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
...@@ -307,6 +331,10 @@ class LoginPresenter @Inject constructor( ...@@ -307,6 +331,10 @@ class LoginPresenter @Inject constructor(
delay(3, TimeUnit.SECONDS) delay(3, TimeUnit.SECONDS)
client.loginWithCas(credentialToken) client.loginWithCas(credentialToken)
} }
TYPE_LOGIN_SAML -> {
delay(3, TimeUnit.SECONDS)
client.loginWithSaml(credentialToken)
}
TYPE_LOGIN_OAUTH -> { TYPE_LOGIN_OAUTH -> {
client.loginWithOauth(credentialToken, credentialSecret) client.loginWithOauth(credentialToken, credentialSecret)
} }
...@@ -319,21 +347,19 @@ class LoginPresenter @Inject constructor( ...@@ -319,21 +347,19 @@ class LoginPresenter @Inject constructor(
} }
} }
else -> { else -> {
throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS, TYPE_LOGIN_OAUTH or TYPE_LOGIN_DEEP_LINK") throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS,TYPE_LOGIN_SAML, TYPE_LOGIN_OAUTH or TYPE_LOGIN_DEEP_LINK")
} }
} }
} }
val username = retryIO("me()") { client.me().username } val username = retryIO("me()") { client.me().username }
if (username != null) { if (username != null) {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username) localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveCurrentServer.save(currentServer)
saveAccount(username) saveAccount(username)
saveToken(token) saveToken(token)
registerPushToken() registerPushToken()
if (loginType == TYPE_LOGIN_USER_EMAIL) { if (loginType == TYPE_LOGIN_USER_EMAIL) {
loginCredentials = Credential.Builder(usernameOrEmail) view.saveSmartLockCredentials(usernameOrEmail, password)
.setPassword(password)
.build()
view.saveSmartLockCredentials(loginCredentials)
} }
navigator.toChatList() navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) { } else if (loginType == TYPE_LOGIN_OAUTH) {
...@@ -365,6 +391,18 @@ class LoginPresenter @Inject constructor( ...@@ -365,6 +391,18 @@ class LoginPresenter @Inject constructor(
}.toString() }.toString()
} }
private fun getSamlServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> {
return listMap.filter { map -> map["service"] == "saml" }
}
private fun getSamlServiceName(service: Map<String, Any>): String {
return service["buttonLabelText"].toString()
}
private fun getSamlProvider(service: Map<String, Any>): String {
return (service["clientConfig"] as Map<*, *>)["provider"].toString()
}
private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> { private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> {
return listMap.filter { map -> map["custom"] == true } return listMap.filter { map -> map["custom"] == true }
} }
...@@ -389,11 +427,11 @@ class LoginPresenter @Inject constructor( ...@@ -389,11 +427,11 @@ class LoginPresenter @Inject constructor(
return service["scope"].toString() return service["scope"].toString()
} }
private fun getCustomOauthButtonColor(service: Map<String, Any>): Int { private fun getServiceButtonColor(service: Map<String, Any>): Int {
return service["buttonColor"].toString().parseColor() return service["buttonColor"].toString().parseColor()
} }
private fun getCustomOauthServiceNameColor(service: Map<String, Any>): Int { private fun getServiceNameColor(service: Map<String, Any>): Int {
return service["buttonLabelColor"].toString().parseColor() return service["buttonLabelColor"].toString().parseColor()
} }
......
...@@ -87,7 +87,7 @@ interface LoginView : LoadingView, MessageView { ...@@ -87,7 +87,7 @@ interface LoginView : LoadingView, MessageView {
* Enables and shows the oauth view if there is login via social accounts enabled by the server settings. * Enables and shows the oauth view if there is login via social accounts enabled by the server settings.
* *
* REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle], * REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab] or [addCustomOauthServiceButton]) for the oauth view. * [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab], [addCustomOauthServiceButton] or [addSamlServiceButton]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s). * If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*/ */
fun enableOauthView() fun enableOauthView()
...@@ -197,7 +197,7 @@ interface LoginView : LoadingView, MessageView { ...@@ -197,7 +197,7 @@ interface LoginView : LoadingView, MessageView {
* @state A random string generated by the app, which you'll verify later (to protect against forgery attacks). * @state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
* @serviceName The custom OAuth service name. * @serviceName The custom OAuth service name.
* @serviceNameColor The custom OAuth service name color (just stylizing). * @serviceNameColor The custom OAuth service name color (just stylizing).
* @buttonColor The color of the custom OAuth button (just stylizing). * @buttonColor The custom OAuth button color (just stylizing).
* @see [enableOauthView] * @see [enableOauthView]
*/ */
fun addCustomOauthServiceButton( fun addCustomOauthServiceButton(
...@@ -208,6 +208,23 @@ interface LoginView : LoadingView, MessageView { ...@@ -208,6 +208,23 @@ interface LoginView : LoadingView, MessageView {
buttonColor: Int buttonColor: Int
) )
/**
* Adds a SAML button in the oauth view.
*
* @samlUrl The SAML url to sets up the button (the listener).
* @serviceName The SAML service name.
* @serviceNameColor The SAML service name color (just stylizing).
* @buttonColor The SAML button color (just stylizing).
* @see [enableOauthView]
*/
fun addSamlServiceButton(
samlUrl: String,
samlToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/ */
...@@ -226,7 +243,7 @@ interface LoginView : LoadingView, MessageView { ...@@ -226,7 +243,7 @@ interface LoginView : LoadingView, MessageView {
fun alertWrongPassword() fun alertWrongPassword()
/** /**
* Save credentials via google smart lock * Saves Google Smart Lock credentials.
*/ */
fun saveSmartLockCredentials(loginCredential: Credential?) fun saveSmartLockCredentials(id: String, password: String)
} }
\ No newline at end of file
...@@ -27,7 +27,8 @@ class RegisterUsernamePresenter @Inject constructor( ...@@ -27,7 +27,8 @@ class RegisterUsernamePresenter @Inject constructor(
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor settingsInteractor: GetSettingsInteractor
) { ) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
...@@ -47,6 +48,7 @@ class RegisterUsernamePresenter @Inject constructor( ...@@ -47,6 +48,7 @@ class RegisterUsernamePresenter @Inject constructor(
val registeredUsername = me.username val registeredUsername = me.username
if (registeredUsername != null) { if (registeredUsername != null) {
saveAccount(registeredUsername) saveAccount(registeredUsername)
saveCurrentServer.save(currentServer)
tokenRepository.save(currentServer, Token(userId, authToken)) tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.resetpassword.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy 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.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail import chat.rocket.android.util.extensions.isEmail
...@@ -19,7 +20,7 @@ class ResetPasswordPresenter @Inject constructor( ...@@ -19,7 +20,7 @@ class ResetPasswordPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
factory: RocketChatClientFactory, factory: RocketChatClientFactory,
serverInteractor: GetCurrentServerInteractor serverInteractor: GetConnectingServerInteractor
) { ) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
......
...@@ -6,7 +6,7 @@ import chat.rocket.android.core.behaviours.showMessage ...@@ -6,7 +6,7 @@ import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor 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.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl import chat.rocket.android.util.extensions.isValidUrl
...@@ -16,7 +16,7 @@ import javax.inject.Inject ...@@ -16,7 +16,7 @@ import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView, class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor, private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory factory: RocketChatClientFactory
......
...@@ -15,7 +15,6 @@ import chat.rocket.core.internal.rest.login ...@@ -15,7 +15,6 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.signup import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import com.google.android.gms.auth.api.credentials.Credential
import javax.inject.Inject import javax.inject.Inject
class SignupPresenter @Inject constructor( class SignupPresenter @Inject constructor(
...@@ -23,7 +22,8 @@ class SignupPresenter @Inject constructor( ...@@ -23,7 +22,8 @@ class SignupPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
...@@ -61,13 +61,11 @@ class SignupPresenter @Inject constructor( ...@@ -61,13 +61,11 @@ class SignupPresenter @Inject constructor(
// TODO This function returns a user token so should we save it? // TODO This function returns a user token so should we save it?
retryIO("login") { client.login(username, password) } retryIO("login") { client.login(username, password) }
val me = retryIO("me") { client.me() } val me = retryIO("me") { client.me() }
saveCurrentServerInteractor.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username) localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me) saveAccount(me)
registerPushToken() registerPushToken()
val loginCredentials = Credential.Builder(email) view.saveSmartLockCredentials(username, password)
.setPassword(password)
.build()
view.saveSmartLockCredentials(loginCredentials)
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { exception.message?.let {
......
...@@ -27,7 +27,7 @@ interface SignupView : LoadingView, MessageView { ...@@ -27,7 +27,7 @@ interface SignupView : LoadingView, MessageView {
fun alertBlankEmail() fun alertBlankEmail()
/** /**
* Save credentials via google smart lock * Saves Google Smart Lock credentials.
*/ */
fun saveSmartLockCredentials(loginCredential: Credential) fun saveSmartLockCredentials(id: String, password: String)
} }
\ No newline at end of file
package chat.rocket.android.authentication.signup.ui package chat.rocket.android.authentication.signup.ui
import DrawableHelper import DrawableHelper
import android.app.Activity.RESULT_OK import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
...@@ -11,30 +11,24 @@ import android.view.LayoutInflater ...@@ -11,30 +11,24 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.googleApiClient import chat.rocket.android.R.string.message_credentials_saved_successfully
import chat.rocket.android.authentication.signup.presentation.SignupPresenter import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.SmartLockHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.credentials.Credentials
import com.google.android.gms.auth.api.credentials.Credential
import com.google.android.gms.common.api.ResolvingResultCallbacks
import com.google.android.gms.common.api.Status
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.* import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal const val SAVE_CREDENTIALS = 1 internal const val SAVE_CREDENTIALS = 1
class SignupFragment : Fragment(), SignupView { class SignupFragment : Fragment(), SignupView {
@Inject @Inject
lateinit var presenter: SignupPresenter lateinit var presenter: SignupPresenter
private lateinit var credentialsToBeSaved: Credential
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) { if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false) bottom_container.setVisible(false)
...@@ -120,44 +114,16 @@ class SignupFragment : Fragment(), SignupView { ...@@ -120,44 +114,16 @@ class SignupFragment : Fragment(), SignupView {
} }
} }
override fun saveSmartLockCredentials(loginCredential: Credential) {
credentialsToBeSaved = loginCredential
googleApiClient.let {
if (it.isConnected) {
saveCredentials()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SAVE_CREDENTIALS) { if (resultCode == Activity.RESULT_OK) {
if (resultCode == RESULT_OK) { if (data != null) {
Toast.makeText( if (requestCode == SAVE_CREDENTIALS) {
context, showMessage(getString(message_credentials_saved_successfully))
getString(R.string.message_credentials_saved_successfully), }
Toast.LENGTH_SHORT
).show()
} else {
Timber.e("ERROR: Cancelled by user")
} }
} }
} }
private fun saveCredentials() {
activity?.let {
Auth.CredentialsApi.save(googleApiClient, credentialsToBeSaved).setResultCallback(
object : ResolvingResultCallbacks<Status>(it, SAVE_CREDENTIALS) {
override fun onSuccess(status: Status) {
Timber.d("save:SUCCESS:$status")
}
override fun onUnresolvableFailure(status: Status) {
Timber.e("save:FAILURE:$status")
}
})
}
}
override fun showLoading() { override fun showLoading() {
ui { ui {
enableUserInput(false) enableUserInput(false)
...@@ -188,6 +154,12 @@ class SignupFragment : Fragment(), SignupView { ...@@ -188,6 +154,12 @@ class SignupFragment : Fragment(), SignupView {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
override fun saveSmartLockCredentials(id: String, password: String) {
activity?.let {
SmartLockHelper.save(Credentials.getClient(it), it, id, password)
}
}
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
ui { ui {
val personDrawable = val personDrawable =
......
...@@ -25,7 +25,8 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -25,7 +25,8 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
...@@ -55,6 +56,7 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -55,6 +56,7 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
} }
val me = retryIO("me") { client.me() } val me = retryIO("me") { client.me() }
saveAccount(me) saveAccount(me)
saveCurrentServerInteractor.save(currentServer)
tokenRepository.save(server, token) tokenRepository.save(server, token)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
......
...@@ -32,19 +32,27 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -32,19 +32,27 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setContentView(R.layout.activity_authentication) setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme) setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
val deepLinkInfo = intent.getLoginDeepLinkInfo() val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) { launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false) val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
// if we got authenticateWithDeepLink information, pass true to newServer also // if we got authenticateWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated -> presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) { if (!authenticated) {
showServerInput(savedInstanceState, deepLinkInfo) showServerInput(deepLinkInfo)
} }
} }
} }
} }
override fun onStop() {
job.cancel()
super.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
...@@ -53,17 +61,12 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -53,17 +61,12 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
} }
} }
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> { override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) { fun showServerInput(deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) { addFragment("ServerFragment", R.id.fragment_container, allowStateLoss = true) {
ServerFragment.newInstance(deepLinkInfo) ServerFragment.newInstance(deepLinkInfo)
} }
} }
......
...@@ -30,7 +30,7 @@ class ImageAttachmentViewHolder( ...@@ -30,7 +30,7 @@ class ImageAttachmentViewHolder(
file_name.text = data.attachmentTitle file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { image_attachment.setOnClickListener {
ImageHelper.openImage( ImageHelper.openImage(
it.context, context,
data.attachmentUrl, data.attachmentUrl,
data.attachmentTitle.toString() data.attachmentTitle.toString()
) )
......
package chat.rocket.android.chatroom.di package chat.rocket.android.chatroom.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.presentation.ChatRoomView 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.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
......
...@@ -181,9 +181,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -181,9 +181,7 @@ class ChatRoomPresenter @Inject constructor(
} }
subscribeTypingStatus() subscribeTypingStatus()
if (offset == 0L) { subscribeState()
subscribeState()
}
} }
} }
...@@ -229,10 +227,9 @@ class ChatRoomPresenter @Inject constructor( ...@@ -229,10 +227,9 @@ class ChatRoomPresenter @Inject constructor(
) )
try { try {
messagesRepository.save(newMessage) messagesRepository.save(newMessage)
val message = client.sendMessage(id, chatRoomId, text)
view.showNewMessage(mapper.map(newMessage, RoomViewModel( view.showNewMessage(mapper.map(newMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast))) roles = chatRoles, isBroadcast = chatIsBroadcast)))
message client.sendMessage(id, chatRoomId, text)
} catch (ex: Exception) { } catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response // Ok, not very beautiful, but the backend sends us a not valid response
// When someone sends a message on a read-only channel, so we just ignore it // When someone sends a message on a read-only channel, so we just ignore it
...@@ -324,7 +321,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -324,7 +321,7 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
private fun subscribeState() { private suspend fun subscribeState() {
Timber.d("Subscribing to Status changes") Timber.d("Subscribing to Status changes")
lastState = manager.state lastState = manager.state
manager.addStatusChannel(stateChannel) manager.addStatusChannel(stateChannel)
...@@ -790,12 +787,14 @@ class ChatRoomPresenter @Inject constructor( ...@@ -790,12 +787,14 @@ class ChatRoomPresenter @Inject constructor(
} }
private suspend fun subscribeTypingStatus() { private suspend fun subscribeTypingStatus() {
client.subscribeTypingStatus(chatRoomId.toString()) { _, id -> launch(CommonPool + strategy.jobs) {
typingStatusSubscriptionId = id client.subscribeTypingStatus(chatRoomId.toString()) { _, id ->
} typingStatusSubscriptionId = id
}
for (typingStatus in client.typingStatusChannel) { for (typingStatus in client.typingStatusChannel) {
processTypingStatus(typingStatus) processTypingStatus(typingStatus)
}
} }
} }
...@@ -813,7 +812,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -813,7 +812,7 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
if (typingStatusList.isNotEmpty()) { if (typingStatusList.isNotEmpty()) {
view.showTypingStatus(typingStatusList) view.showTypingStatus(typingStatusList.toList()) // copy typingStatusList
} else { } else {
view.hideTypingStatusView() view.hideTypingStatusView()
} }
...@@ -837,6 +836,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -837,6 +836,7 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel( val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast)) roles = chatRoles, isBroadcast = chatIsBroadcast))
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId) val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id } val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) { if (index > -1) {
......
...@@ -31,7 +31,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -31,7 +31,7 @@ interface ChatRoomView : LoadingView, MessageView {
* *
* @param usernameList The list of username to show. * @param usernameList The list of username to show.
*/ */
fun showTypingStatus(usernameList: ArrayList<String>) fun showTypingStatus(usernameList: List<String>)
/** /**
* Hides the typing status view. * Hides the typing status view.
......
...@@ -64,7 +64,7 @@ class MessageService : JobService() { ...@@ -64,7 +64,7 @@ class MessageService : JobService() {
Timber.e(ex) Timber.e(ex)
// TODO - remove the generic message when we implement :userId:/message subscription // TODO - remove the generic message when we implement :userId:/message subscription
if (ex is IllegalStateException) { if (ex is IllegalStateException) {
Timber.d(ex, "Probably a read-only problem...") Timber.e(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed. // TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id) messageRepository.removeById(message.id)
jobFinished(params, false) jobFinished(params, false)
......
...@@ -31,6 +31,8 @@ import chat.rocket.android.helper.KeyboardHelper ...@@ -31,6 +31,8 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.* import chat.rocket.android.widget.emoji.*
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
...@@ -243,7 +245,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -243,7 +245,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter( adapter = ChatRoomAdapter(
chatRoomType, chatRoomName, presenter, chatRoomType,
chatRoomName,
presenter,
reactionListener = this@ChatRoomFragment reactionListener = this@ChatRoomFragment
) )
recycler_view.adapter = adapter recycler_view.adapter = adapter
...@@ -261,7 +265,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -261,7 +265,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
verticalScrollOffset.set(0) verticalScrollOffset.set(0)
} }
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true) presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
toggleNoChatView(adapter.itemCount) empty_chat_view.isVisible = adapter.itemCount == 0
} }
} }
...@@ -272,27 +276,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -272,27 +276,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
) { ) {
// TODO: We should rely solely on the user being able to post, but we cannot guarantee // TODO: We should rely solely on the user being able to post, but we cannot guarantee
// that the "(channels|groups).roles" endpoint is supported by the server in use. // that the "(channels|groups).roles" endpoint is supported by the server in use.
setupMessageComposer(userCanPost) ui {
isBroadcastChannel = channelIsBroadcast setupMessageComposer(userCanPost)
if (isBroadcastChannel && !userCanMod) activity?.invalidateOptionsMenu() isBroadcastChannel = channelIsBroadcast
if (isBroadcastChannel && !userCanMod) activity?.invalidateOptionsMenu()
}
} }
override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) { override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {
} }
private fun toggleNoChatView(size: Int) {
if (size == 0) {
image_chat_icon.setVisible(true)
text_chat_title.setVisible(true)
text_chat_description.setVisible(true)
} else {
image_chat_icon.setVisible(false)
text_chat_title.setVisible(false)
text_chat_description.setVisible(false)
}
}
private val layoutChangeListener = private val layoutChangeListener =
View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom val y = oldBottom - bottom
...@@ -361,7 +355,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -361,7 +355,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun showTypingStatus(usernameList: ArrayList<String>) { override fun showTypingStatus(usernameList: List<String>) {
ui { ui {
when (usernameList.size) { when (usernameList.size) {
1 -> { 1 -> {
...@@ -406,7 +400,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -406,7 +400,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
adapter.prependData(message) adapter.prependData(message)
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0) verticalScrollOffset.set(0)
toggleNoChatView(adapter.itemCount) empty_chat_view.isVisible = adapter.itemCount == 0
} }
} }
...@@ -507,6 +501,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -507,6 +501,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
ui { ui {
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", message) clipboard.primaryClip = ClipData.newPlainText("", message)
showToast(R.string.msg_message_copied)
} }
} }
...@@ -646,12 +641,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -646,12 +641,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (isChatRoomReadOnly && !canPost) { if (isChatRoomReadOnly && !canPost) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else if (!isSubscribed) { } else if (!isSubscribed && roomTypeOf(chatRoomType) !is RoomType.DirectMessage) {
input_container.setVisible(false) input_container.setVisible(false)
button_join_chat.setVisible(true) button_join_chat.setVisible(true)
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) } button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else { } else {
button_send.alpha = 0f
button_send.setVisible(false) button_send.setVisible(false)
button_show_attachment_options.alpha = 1f button_show_attachment_options.alpha = 1f
button_show_attachment_options.setVisible(true) button_show_attachment_options.setVisible(true)
...@@ -787,14 +781,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -787,14 +781,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupComposeButtons(charSequence: CharSequence) { private fun setupComposeButtons(charSequence: CharSequence) {
if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) { if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
button_show_attachment_options.fadeOut(1F, 0F, 120) button_show_attachment_options.setVisible(false)
button_send.fadeIn(0F, 1F, 120) button_send.setVisible(true)
playComposeMessageButtonsAnimation = false playComposeMessageButtonsAnimation = false
} }
if (charSequence.isEmpty()) { if (charSequence.isEmpty()) {
button_send.fadeOut(1F, 0F, 120) button_send.setVisible(false)
button_show_attachment_options.fadeIn(0F, 1F, 120) button_show_attachment_options.setVisible(true)
playComposeMessageButtonsAnimation = true playComposeMessageButtonsAnimation = true
} }
} }
......
...@@ -4,19 +4,19 @@ import chat.rocket.android.R ...@@ -4,19 +4,19 @@ import chat.rocket.android.R
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
data class MessageViewModel( data class MessageViewModel(
override val message: Message, override val message: Message,
override val rawData: Message, override val rawData: Message,
override val messageId: String, override val messageId: String,
override val avatar: String, override val avatar: String,
override val time: CharSequence, override val time: CharSequence,
override val senderName: CharSequence, override val senderName: CharSequence,
override val content: CharSequence, override val content: CharSequence,
override val isPinned: Boolean, override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null, override var preview: Message? = null,
var isFirstUnread: Boolean, var isFirstUnread: Boolean,
override var isTemporary: Boolean = false override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
data class ReactionViewModel( data class ReactionViewModel(
val messageId: String, val messageId: String,
val shortname: String, val shortname: String,
val unicode: CharSequence, val unicode: CharSequence,
val count: Int, val count: Int,
val usernames: List<String> = emptyList() val usernames: List<String> = emptyList()
) )
\ No newline at end of file
...@@ -14,6 +14,7 @@ import androidx.core.text.color ...@@ -14,6 +14,7 @@ import androidx.core.text.color
import androidx.core.text.scale import androidx.core.text.scale
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply 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.MessageHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
...@@ -47,6 +48,7 @@ import okhttp3.HttpUrl ...@@ -47,6 +48,7 @@ import okhttp3.HttpUrl
import java.security.InvalidParameterException import java.security.InvalidParameterException
import javax.inject.Inject import javax.inject.Inject
@PerFragment
class ViewModelMapper @Inject constructor( class ViewModelMapper @Inject constructor(
private val context: Context, private val context: Context,
private val parser: MessageParser, private val parser: MessageParser,
...@@ -59,76 +61,137 @@ class ViewModelMapper @Inject constructor( ...@@ -59,76 +61,137 @@ class ViewModelMapper @Inject constructor(
) { ) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer) private val settings = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl() private val baseUrl = currentServer
private val token = tokenRepository.get(currentServer) private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY) private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText) private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message, roomViewModel: RoomViewModel = RoomViewModel( suspend fun map(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> { message: Message,
roomViewModel: RoomViewModel = RoomViewModel(roles = emptyList(), isBroadcast = true)
): List<BaseViewModel<*>> {
return translate(message, roomViewModel) return translate(message, roomViewModel)
} }
suspend fun map(messages: List<Message>, roomViewModel: RoomViewModel = RoomViewModel( suspend fun map(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> = withContext(CommonPool) { messages: List<Message>,
val list = ArrayList<BaseViewModel<*>>(messages.size) roomViewModel: RoomViewModel = RoomViewModel(roles = emptyList(), isBroadcast = true),
asNotReversed: Boolean = false
messages.forEach { ): List<BaseViewModel<*>> =
list.addAll(translate(it, roomViewModel)) withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>(messages.size)
messages.forEach {
list.addAll(
if (asNotReversed) translateAsNotReversed(it, roomViewModel)
else translate(it, roomViewModel)
)
}
return@withContext list
} }
return@withContext list private suspend fun translate(
} message: Message,
roomViewModel: RoomViewModel
): List<BaseViewModel<*>> =
withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
private suspend fun translate(message: Message, roomViewModel: RoomViewModel) message.urls?.forEach {
: List<BaseViewModel<*>> = withContext(CommonPool) { val url = mapUrl(message, it)
val list = ArrayList<BaseViewModel<*>>() url?.let { list.add(url) }
}
message.urls?.forEach { message.attachments?.forEach {
val url = mapUrl(message, it) val attachment = mapAttachment(message, it)
url?.let { list.add(url) } attachment?.let { list.add(attachment) }
} }
message.attachments?.forEach { mapMessage(message).let {
val attachment = mapAttachment(message, it) if (list.isNotEmpty()) {
attachment?.let { list.add(attachment) } it.preview = list.first().preview
} }
list.add(it)
}
mapMessage(message).let { for (i in list.size - 1 downTo 0) {
if (list.isNotEmpty()) { val next = if (i - 1 < 0) null else list[i - 1]
it.preview = list.first().preview list[i].nextDownStreamMessage = next
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
} }
list.add(it)
}
for (i in list.size - 1 downTo 0) { return@withContext list
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
} }
if (isBroadcastReplyAvailable(roomViewModel, message)) { private suspend fun translateAsNotReversed(
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom -> message: Message,
val replyViewModel = mapMessageReply(message, chatRoom) roomViewModel: RoomViewModel
list.first().nextDownStreamMessage = replyViewModel ): List<BaseViewModel<*>> =
list.add(0, replyViewModel) withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
mapMessage(message).let {
if (list.isNotEmpty()) {
it.preview = list.first().preview
}
list.add(it)
}
message.attachments?.forEach {
val attachment = mapAttachment(message, it)
attachment?.let {
list.add(attachment)
}
} }
}
return@withContext list message.urls?.forEach {
} val url = mapUrl(message, it)
url?.let {
list.add(url)
}
}
for (i in list.size - 1 downTo 0) {
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
}
list.dropLast(1).forEach {
it.reactions = emptyList()
}
list.last().reactions = getReactions(message)
list.last().nextDownStreamMessage = null
return@withContext list
}
private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean { private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean {
val senderUsername = message.sender?.username val senderUsername = message.sender?.username
return roomViewModel.isRoom && roomViewModel.isBroadcast && return roomViewModel.isRoom && roomViewModel.isBroadcast &&
!message.isSystemMessage() && !message.isSystemMessage() &&
senderUsername != currentUsername senderUsername != currentUsername
} }
private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel { private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel {
val name = message.sender?.name val name = message.sender?.name
val roomName = if (settings.useRealName() && name != null) name else message.sender?.username val roomName =
?: "" if (settings.useRealName() && name != null) name else message.sender?.username ?: ""
val permalink = messageHelper.createPermalink(message, chatRoom) val permalink = messageHelper.createPermalink(message, chatRoom)
return MessageReplyViewModel( return MessageReplyViewModel(
messageId = message.id, messageId = message.id,
......
...@@ -14,6 +14,7 @@ import chat.rocket.common.RocketChatException ...@@ -14,6 +14,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.createDirectMessage
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -48,6 +49,17 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -48,6 +49,17 @@ class ChatRoomsPresenter @Inject constructor(
if (myself?.username == null) { if (myself?.username == null) {
view.showMessage(R.string.msg_generic_error) view.showMessage(R.string.msg_generic_error)
} else { } else {
val id = if (isDirectMessage && !open) {
retryIO("createDirectMessage($name)") {
client.createDirectMessage(name)
}
val fromTo = mutableListOf(myself.id, id).apply {
sort()
}
fromTo.joinToString("")
} else {
id
}
val isChatRoomOwner = ownerId == myself.id || isDirectMessage val isChatRoomOwner = ownerId == myself.id || isDirectMessage
navigator.toChatRoom(id, roomName, type, readonly ?: false, navigator.toChatRoom(id, roomName, type, readonly ?: false,
lastSeen ?: -1, open, isChatRoomOwner) lastSeen ?: -1, open, isChatRoomOwner)
......
...@@ -45,6 +45,8 @@ import kotlinx.coroutines.experimental.launch ...@@ -45,6 +45,8 @@ import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
class ChatRoomsFragment : Fragment(), ChatRoomsView { class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject @Inject
lateinit var presenter: ChatRoomsPresenter lateinit var presenter: ChatRoomsPresenter
...@@ -58,14 +60,31 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -58,14 +60,31 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler() private val handler = Handler()
private var chatRoomId: String? = null
companion object { companion object {
fun newInstance() = ChatRoomsFragment() fun newInstance(chatRoomId: String? = null): ChatRoomsFragment {
return ChatRoomsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
setHasOptionsMenu(true) setHasOptionsMenu(true)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId?.let {
// TODO - bring back support to load a room from id.
//presenter.goToChatRoomWithId(it)
chatRoomId = null
}
}
} }
override fun onDestroy() { override fun onDestroy() {
......
...@@ -12,6 +12,7 @@ import chat.rocket.android.R ...@@ -12,6 +12,7 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService 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.dagger.qualifier.ForMessages
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
...@@ -42,8 +43,10 @@ import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepos ...@@ -42,8 +43,10 @@ import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepos
import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesPermissionsRepository import chat.rocket.android.server.infraestructure.SharedPreferencesPermissionsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsConnectingServerRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.HttpLoggingInterceptor
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date import chat.rocket.common.internal.ISO8601Date
...@@ -61,7 +64,6 @@ import com.squareup.moshi.Moshi ...@@ -61,7 +64,6 @@ import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.spans.SpannableTheme import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber import timber.log.Timber
...@@ -80,8 +82,10 @@ class AppModule { ...@@ -80,8 +82,10 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message -> val interceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
Timber.d(message) override fun log(message: String) {
Timber.d(message)
}
}) })
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY interceptor.level = HttpLoggingInterceptor.Level.BODY
...@@ -156,6 +160,12 @@ class AppModule { ...@@ -156,6 +160,12 @@ class AppModule {
return SharedPrefsCurrentServerRepository(prefs) return SharedPrefsCurrentServerRepository(prefs)
} }
@Provides
@ForAuthentication
fun provideConnectingServerRepository(prefs: SharedPreferences): CurrentServerRepository {
return SharedPrefsConnectingServerRepository(prefs)
}
@Provides @Provides
@Singleton @Singleton
fun provideSettingsRepository(localRepository: LocalRepository): SettingsRepository { fun provideSettingsRepository(localRepository: LocalRepository): SettingsRepository {
......
...@@ -2,10 +2,10 @@ package chat.rocket.android.dagger.module ...@@ -2,10 +2,10 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.push.FirebaseMessagingService
import chat.rocket.android.push.FirebaseTokenService import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.di.FirebaseMessagingServiceProvider
import chat.rocket.android.push.di.FirebaseTokenServiceProvider import chat.rocket.android.push.di.FirebaseTokenServiceProvider
import chat.rocket.android.push.di.GcmListenerServiceProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -14,8 +14,8 @@ import dagger.android.ContributesAndroidInjector ...@@ -14,8 +14,8 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class]) @ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class])
abstract fun bindFirebaseTokenService(): FirebaseTokenService abstract fun bindFirebaseTokenService(): FirebaseTokenService
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class]) @ContributesAndroidInjector(modules = [FirebaseMessagingServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService abstract fun bindGcmListenerService(): FirebaseMessagingService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class]) @ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService abstract fun bindMessageService(): MessageService
......
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 ...@@ -2,10 +2,6 @@ package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier import javax.inject.Qualifier
/**
* Created by luciofm on 4/14/18.
*/
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class ForMessages annotation class ForMessages
\ No newline at end of file
...@@ -25,9 +25,9 @@ class FavoriteMessagesPresenter @Inject constructor( ...@@ -25,9 +25,9 @@ class FavoriteMessagesPresenter @Inject constructor(
private var offset: Int = 0 private var offset: Int = 0
/** /**
* Loads all favorite messages for room. the given room id. * Loads all favorite messages for the given room id.
* *
* @param roomId The id of the room to get its favorite messages. * @param roomId The id of the room to get favorite messages from.
*/ */
fun loadFavoriteMessages(roomId: String) { fun loadFavoriteMessages(roomId: String) {
launchUI(strategy) { launchUI(strategy) {
...@@ -35,7 +35,7 @@ class FavoriteMessagesPresenter @Inject constructor( ...@@ -35,7 +35,7 @@ class FavoriteMessagesPresenter @Inject constructor(
view.showLoading() view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let { roomsInteractor.getById(serverUrl, roomId)?.let {
val favoriteMessages = client.getFavoriteMessages(roomId, it.type, offset) val favoriteMessages = client.getFavoriteMessages(roomId, it.type, offset)
val messageList = mapper.map(favoriteMessages.result) val messageList = mapper.map(favoriteMessages.result, asNotReversed = true)
view.showFavoriteMessages(messageList) view.showFavoriteMessages(messageList)
offset += 1 * 30 offset += 1 * 30
}.ifNull { }.ifNull {
......
...@@ -71,7 +71,7 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -71,7 +71,7 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
val linearLayoutManager = LinearLayoutManager(context) val linearLayoutManager = LinearLayoutManager(context)
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
if (favoriteMessages.size > 10) { if (favoriteMessages.size >= 30) {
recycler_view.addOnScrollListener(object : recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) { EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore( override fun onLoadMore(
......
...@@ -85,6 +85,7 @@ class FilesFragment : Fragment(), FilesView { ...@@ -85,6 +85,7 @@ class FilesFragment : Fragment(), FilesView {
} }
}) })
} }
group_no_file.isVisible = dataSet.isEmpty()
} else { } else {
adapter.appendData(dataSet) adapter.appendData(dataSet)
...@@ -93,17 +94,13 @@ class FilesFragment : Fragment(), FilesView { ...@@ -93,17 +94,13 @@ class FilesFragment : Fragment(), FilesView {
override fun playMedia(url: String) { override fun playMedia(url: String) {
ui { ui {
activity?.let { PlayerActivity.play(it, url)
PlayerActivity.play(it, url)
}
} }
} }
override fun openImage(url: String, name: String) { override fun openImage(url: String, name: String) {
ui { ui {
activity?.let { ImageHelper.openImage(root_layout.context, url, name)
ImageHelper.openImage(it, url, name)
}
} }
} }
......
...@@ -41,9 +41,10 @@ class FileViewModel( ...@@ -41,9 +41,10 @@ class FileViewModel(
} }
private fun getFileUploadDate(): String { private fun getFileUploadDate(): String {
return DateTimeHelper.getDateTime( genericAttachment.uploadedAt?.let {
DateTimeHelper.getLocalDateTime(genericAttachment.uploadedAt) return DateTimeHelper.getDateTime(DateTimeHelper.getLocalDateTime(it))
) }
return ""
} }
private fun getFileUrl(): String? { private fun getFileUrl(): String? {
......
...@@ -53,7 +53,6 @@ object ImageHelper { ...@@ -53,7 +53,6 @@ object ImageHelper {
) )
val toolbar = Toolbar(context).also { val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions) it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
it.setOnMenuItemClickListener { it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) { return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage(context) R.id.action_save_image -> saveImage(context)
...@@ -109,7 +108,6 @@ object ImageHelper { ...@@ -109,7 +108,6 @@ object ImageHelper {
.hideStatusBar(false) .hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder) .setCustomDraweeControllerBuilder(builder)
.show() .show()
} }
private fun saveImage(context: Context): Boolean { private fun saveImage(context: Context): Boolean {
...@@ -166,5 +164,4 @@ object ImageHelper { ...@@ -166,5 +164,4 @@ object ImageHelper {
) )
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.helper
import android.app.Activity
import android.content.IntentSender
import androidx.fragment.app.FragmentActivity
import com.google.android.gms.auth.api.credentials.*
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.ResolvableApiException
import timber.log.Timber
const val REQUEST_CODE_FOR_SIGN_IN_REQUIRED = 1
const val REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION = 2
const val REQUEST_CODE_FOR_SAVE_RESOLUTION = 3
/**
* This class handles some cases of Google Smart Lock for passwords like the request to retrieve
* credentials, to retrieve sign-in hints and to store the credentials.
*
* See https://developers.google.com/identity/smartlock-passwords/android/overview for futher
* information.
*/
object SmartLockHelper {
/**
* Requests for stored Google Smart Lock credentials.
* Note that in case of exception it will try to start a sign in
* ([REQUEST_CODE_FOR_SIGN_IN_REQUIRED]) or "multiple account"
* ([REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION]) resolution.
*
* @param credentialsClient The credential client.
* @param activity The activity.
* @return null or the [Credential] result.
*/
fun requestStoredCredentials(
credentialsClient: CredentialsClient,
activity: Activity
): Credential? {
var credential: Credential? = null
val credentialRequest = CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.build()
credentialsClient.request(credentialRequest)
.addOnCompleteListener {
when {
it.isSuccessful -> {
credential = it.result.credential
}
it.exception is ResolvableApiException -> {
val resolvableApiException = (it.exception as ResolvableApiException)
if (resolvableApiException.statusCode == CommonStatusCodes.SIGN_IN_REQUIRED) {
provideSignInHint(credentialsClient, activity)
} else {
// This is most likely the case where the user has multiple saved
// credentials and needs to pick one. This requires showing UI to
// resolve the read request.
resolveResult(
resolvableApiException,
REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION,
activity
)
}
}
}
}
return credential
}
/**
* Saves a user credential to Google Smart Lock.
* Note that in case of exception it will try to start a save resolution,
* so the activity/fragment should expected for a request code
* ([REQUEST_CODE_FOR_SAVE_RESOLUTION]) on onActivityResult call.
*
* @param credentialsClient The credential client.
* @param activity The activity.
* @param id The user id credential.
* @param password The user password credential.
*/
fun save(
credentialsClient: CredentialsClient,
activity: FragmentActivity,
id: String,
password: String
) {
val credential = Credential.Builder(id)
.setPassword(password)
.build()
credentialsClient.save(credential)
.addOnCompleteListener {
val exception = it.exception
if (exception is ResolvableApiException) {
// Try to resolve the save request. This will prompt the user if
// the credential is new.
try {
exception.startResolutionForResult(
activity,
REQUEST_CODE_FOR_SAVE_RESOLUTION
)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Failed to send resolution. Exception is: $e")
}
}
}
}
private fun provideSignInHint(credentialsClient: CredentialsClient, activity: Activity) {
val hintRequest = HintRequest.Builder()
.setHintPickerConfig(
CredentialPickerConfig.Builder()
.setShowCancelButton(true)
.build()
)
.setEmailAddressIdentifierSupported(true)
.build()
try {
val intent = credentialsClient.getHintPickerIntent(hintRequest)
activity.startIntentSenderForResult(
intent.intentSender,
REQUEST_CODE_FOR_SIGN_IN_REQUIRED,
null,
0,
0,
0,
null
)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Could not start hint picker Intent. Exception is: $e")
}
}
private fun resolveResult(
exception: ResolvableApiException,
requestCode: Int,
activity: Activity
) {
try {
exception.startResolutionForResult(activity, requestCode)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Failed to send resolution. Exception is: $e")
}
}
}
\ 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
...@@ -12,9 +12,9 @@ import chat.rocket.android.util.extensions.addFragment ...@@ -12,9 +12,9 @@ import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity) { class MainNavigator(internal val activity: MainActivity) {
fun toChatList() { fun toChatList(chatRoomId: String? = null) {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) { activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
ChatRoomsFragment.newInstance() ChatRoomsFragment.newInstance(chatRoomId)
} }
} }
...@@ -43,7 +43,7 @@ class MainNavigator(internal val activity: MainActivity) { ...@@ -43,7 +43,7 @@ class MainNavigator(internal val activity: MainActivity) {
} }
fun toNewServer(serverUrl: String? = null) { fun toNewServer(serverUrl: String? = null) {
activity.startActivity(activity.changeServerIntent(serverUrl)) activity.startActivity(activity.changeServerIntent(serverUrl = serverUrl))
activity.finish() activity.finish()
} }
......
...@@ -49,7 +49,7 @@ class MainPresenter @Inject constructor( ...@@ -49,7 +49,7 @@ class MainPresenter @Inject constructor(
private val userDataChannel = Channel<Myself>() private val userDataChannel = Channel<Myself>()
fun toChatList() = navigator.toChatList() fun toChatList(chatRoomId: String? = null) = navigator.toChatList(chatRoomId)
fun toUserProfile() = navigator.toUserProfile() fun toUserProfile() = navigator.toUserProfile()
...@@ -105,13 +105,10 @@ class MainPresenter @Inject constructor( ...@@ -105,13 +105,10 @@ class MainPresenter @Inject constructor(
disconnect() disconnect()
removeAccountInteractor.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
view.disableAutoSignIn()
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...") Timber.d(ex, "Error cleaning up the session...")
} }
view.disableAutoSignIn()
navigator.toNewServer()
} }
} }
...@@ -178,6 +175,7 @@ class MainPresenter @Inject constructor( ...@@ -178,6 +175,7 @@ class MainPresenter @Inject constructor(
if (pushToken != null) { if (pushToken != null) {
try { try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) } retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
view.invalidateToken(pushToken)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error unregistering push token") Timber.d(ex, "Error unregistering push token")
} }
......
...@@ -25,8 +25,5 @@ interface MainView : MessageView, VersionCheckView { ...@@ -25,8 +25,5 @@ interface MainView : MessageView, VersionCheckView {
fun closeServerSelection() fun closeServerSelection()
/** fun invalidateToken(token: String)
* callback to disable auto sign in for google smart lock when the user logs out
*/
fun disableAutoSignIn()
} }
\ No newline at end of file
...@@ -18,15 +18,15 @@ import chat.rocket.android.main.presentation.MainPresenter ...@@ -18,15 +18,15 @@ import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.ui.INTENT_CHAT_ROOM_ID
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import com.google.android.gms.auth.api.Auth import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.messaging.FirebaseMessaging
import com.google.android.gms.common.api.GoogleApiClient import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -40,8 +40,10 @@ import kotlinx.coroutines.experimental.launch ...@@ -40,8 +40,10 @@ import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector, private const val CURRENT_STATE = "current_state"
GoogleApiClient.ConnectionCallbacks {
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
HasSupportFragmentInjector {
@Inject @Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity> lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject @Inject
...@@ -50,72 +52,46 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -50,72 +52,46 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
lateinit var presenter: MainPresenter lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false private var isFragmentAdded: Boolean = false
private var expanded = false private var expanded = false
private lateinit var googleApiClient: GoogleApiClient
private val headerLayout by lazy { view_navigation.getHeaderView(0) } private val headerLayout by lazy { view_navigation.getHeaderView(0) }
private var chatRoomId: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
buildGoogleApiClient()
launch(CommonPool) { launch(CommonPool) {
try { try {
val token = InstanceID.getInstance(this@MainActivity).getToken( val token = FirebaseInstanceId.getInstance().token
getString(R.string.gcm_sender_id), Timber.d("FCM token: $token")
GoogleCloudMessaging.INSTANCE_ID_SCOPE,
null
)
Timber.d("GCM token: $token")
presenter.refreshToken(token) presenter.refreshToken(token)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Missing play services...") Timber.d(ex, "Missing play services...")
} }
} }
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
presenter.connect() presenter.connect()
presenter.loadCurrentInfo() presenter.loadCurrentInfo()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
} }
override fun onConnected(bundle: Bundle?) { override fun onSaveInstanceState(outState: Bundle?) {
} super.onSaveInstanceState(outState)
outState?.putBoolean(CURRENT_STATE, isFragmentAdded)
override fun onConnectionSuspended(errorCode: Int) {
}
private fun buildGoogleApiClient() {
googleApiClient = GoogleApiClient.Builder(this)
.enableAutoManage(this, {
Timber.d("ERROR: connection to client failed")
})
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.build()
} }
override fun onStart() { override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onStart() super.onRestoreInstanceState(savedInstanceState)
googleApiClient.let { isFragmentAdded = savedInstanceState?.getBoolean(CURRENT_STATE) ?: false
if (it.isConnected) {
Timber.d("Google api client connected successfully")
}
}
}
override fun disableAutoSignIn() {
googleApiClient.let {
if (it.isConnected) {
Auth.CredentialsApi.disableAutoSignIn(googleApiClient)
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (!isFragmentAdded) { if (!isFragmentAdded) {
presenter.toChatList() presenter.toChatList(chatRoomId)
isFragmentAdded = true isFragmentAdded = true
} }
} }
...@@ -127,6 +103,12 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -127,6 +103,12 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
} }
} }
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> =
fragmentDispatchingAndroidInjector
override fun showUserStatus(userStatus: UserStatus) { override fun showUserStatus(userStatus: UserStatus) {
headerLayout.apply { headerLayout.apply {
image_user_status.setImageDrawable( image_user_status.setImageDrawable(
...@@ -190,38 +172,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -190,38 +172,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
.show() .show()
} }
private fun setupAccountsList(header: View, accounts: List<Account>) { override fun invalidateToken(token: String) {
accounts_list.layoutManager = LinearLayoutManager(this) FirebaseInstanceId.getInstance().deleteToken(token, FirebaseMessaging.INSTANCE_ID_SCOPE)
accounts_list.adapter = AccountsAdapter(accounts, object : Selector {
override fun onStatusSelected(userStatus: UserStatus) {
presenter.changeDefaultStatus(userStatus)
}
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
header.account_container.setOnClickListener {
header.image_account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
header.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
}
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) = showToast(resId)
...@@ -230,11 +182,6 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -230,11 +182,6 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> =
fragmentDispatchingAndroidInjector
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp)
...@@ -268,4 +215,38 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -268,4 +215,38 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
} }
} }
} }
private fun setupAccountsList(header: View, accounts: List<Account>) {
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(accounts, object : Selector {
override fun onStatusSelected(userStatus: UserStatus) {
presenter.changeDefaultStatus(userStatus)
}
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
header.account_container.setOnClickListener {
header.image_account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
header.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
}
}
} }
\ No newline at end of file
...@@ -26,7 +26,7 @@ class PinnedMessagesPresenter @Inject constructor( ...@@ -26,7 +26,7 @@ class PinnedMessagesPresenter @Inject constructor(
private var offset: Int = 0 private var offset: Int = 0
/** /**
* Load all pinned messages for the given room id. * Loads all pinned messages for the given room id.
* *
* @param roomId The id of the room to get pinned messages from. * @param roomId The id of the room to get pinned messages from.
*/ */
...@@ -36,8 +36,7 @@ class PinnedMessagesPresenter @Inject constructor( ...@@ -36,8 +36,7 @@ class PinnedMessagesPresenter @Inject constructor(
view.showLoading() view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let { roomsInteractor.getById(serverUrl, roomId)?.let {
val pinnedMessages = client.getPinnedMessages(roomId, it.type, offset) val pinnedMessages = client.getPinnedMessages(roomId, it.type, offset)
val messageList = val messageList = mapper.map(pinnedMessages.result, asNotReversed = true)
mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showPinnedMessages(messageList) view.showPinnedMessages(messageList)
offset += 1 * 30 offset += 1 * 30
}.ifNull { }.ifNull {
......
...@@ -74,7 +74,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -74,7 +74,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
val linearLayoutManager = LinearLayoutManager(context) val linearLayoutManager = LinearLayoutManager(context)
recycler_view_pinned.layoutManager = linearLayoutManager recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator() recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10) { if (pinnedMessages.size >= 30) {
recycler_view_pinned.addOnScrollListener(object : recycler_view_pinned.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) { EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore( override fun onLoadMore(
......
...@@ -58,7 +58,8 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -58,7 +58,8 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
if(avatarUrl!="") { if(avatarUrl!="") {
retryIO { client.setAvatar(avatarUrl) } retryIO { client.setAvatar(avatarUrl) }
} }
val user = retryIO { client.updateProfile(myselfId, email, name, username) } val user = retryIO { client.updateProfile(
userId = myselfId, email = email, name = name, username = username) }
view.showProfileUpdateSuccessfullyMessage() view.showProfileUpdateSuccessfullyMessage()
loadUserProfile() loadUserProfile()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
......
...@@ -6,6 +6,7 @@ import android.os.Bundle ...@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter import chat.rocket.android.profile.presentation.ProfilePresenter
...@@ -14,7 +15,6 @@ import chat.rocket.android.util.extensions.* ...@@ -14,7 +15,6 @@ import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.avatar_profile.* import kotlinx.android.synthetic.main.avatar_profile.*
import kotlinx.android.synthetic.main.fragment_profile.* import kotlinx.android.synthetic.main.fragment_profile.*
import javax.inject.Inject import javax.inject.Inject
...@@ -135,7 +135,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -135,7 +135,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as MainActivity).toolbar.title = getString(R.string.title_profile) (activity as AppCompatActivity?)?.supportActionBar?.title = getString(R.string.title_profile)
} }
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
......
package chat.rocket.android.push package chat.rocket.android.push
import android.os.Bundle import androidx.core.os.bundleOf
import com.google.android.gms.gcm.GcmListenerService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import javax.inject.Inject import javax.inject.Inject
class GcmListenerService : GcmListenerService() { class FirebaseMessagingService : FirebaseMessagingService() {
@Inject @Inject
lateinit var pushManager: PushManager lateinit var pushManager: PushManager
...@@ -15,9 +16,9 @@ class GcmListenerService : GcmListenerService() { ...@@ -15,9 +16,9 @@ class GcmListenerService : GcmListenerService() {
AndroidInjection.inject(this) AndroidInjection.inject(this)
} }
override fun onMessageReceived(from: String?, data: Bundle?) { override fun onMessageReceived(message: RemoteMessage) {
data?.let { message.data?.let {
pushManager.handle(data) pushManager.handle(bundleOf(*(it.map { Pair(it.key, it.value) }).toTypedArray()))
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.push package chat.rocket.android.push
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.firebase.iid.FirebaseInstanceId
import com.google.android.gms.iid.InstanceID
import com.google.firebase.iid.FirebaseInstanceIdService import com.google.firebase.iid.FirebaseInstanceIdService
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
...@@ -31,21 +29,18 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -31,21 +29,18 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
} }
override fun onTokenRefresh() { override fun onTokenRefresh() {
//TODO: We need to use the Cordova Project gcm_sender_id since it's the one configured on RC
// default push gateway. We should register this project's own project sender id into it.
try { try {
val gcmToken = InstanceID.getInstance(this) val fcmToken = FirebaseInstanceId.getInstance().token
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get() val currentServer = getCurrentServerInteractor.get()
val client = currentServer?.let { factory.create(currentServer) } val client = currentServer?.let { factory.create(currentServer) }
gcmToken?.let { fcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken) localRepository.save(LocalRepository.KEY_PUSH_TOKEN, fcmToken)
client?.let { client?.let {
launch { launch {
try { try {
Timber.d("Registering push token: $gcmToken for ${client.url}") Timber.d("Registering push token: $fcmToken for ${client.url}")
retryIO("register push token") { client.registerPushToken(gcmToken) } retryIO("register push token") { client.registerPushToken(fcmToken) }
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex, "Error registering push token") Timber.e(ex, "Error registering push token")
} }
...@@ -53,7 +48,7 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -53,7 +48,7 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error refreshing Firebase TOKEN") Timber.e(ex, "Error refreshing Firebase TOKEN")
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.push.di package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.FirebaseMessagingService
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@Module abstract class GcmListenerServiceProvider { @Module abstract class FirebaseMessagingServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class]) @ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideGcmListenerService(): GcmListenerService abstract fun provideFirebaseMessagingService(): FirebaseMessagingService
} }
\ 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 ...@@ -5,11 +5,6 @@ import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>> typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings
}
// Authentication methods. // Authentication methods.
const val LDAP_ENABLE = "LDAP_Enable" const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled" const val CAS_ENABLE = "CAS_enabled"
...@@ -104,4 +99,9 @@ fun PublicSettings.uploadMaxFileSize(): Int { ...@@ -104,4 +99,9 @@ fun PublicSettings.uploadMaxFileSize(): Int {
} }
fun PublicSettings.baseUrl(): String = this[SITE_URL]?.value as String fun PublicSettings.baseUrl(): String = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String? fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
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 ...@@ -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.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter import chat.rocket.core.internal.SettingsAdapter
import timber.log.Timber
class SharedPreferencesSettingsRepository( class SharedPreferencesSettingsRepository(
private val localRepository: LocalRepository private val localRepository: LocalRepository
...@@ -13,11 +14,27 @@ class SharedPreferencesSettingsRepository( ...@@ -13,11 +14,27 @@ class SharedPreferencesSettingsRepository(
private val adapter = SettingsAdapter().lenient() private val adapter = SettingsAdapter().lenient()
override fun save(url: String, settings: PublicSettings) { 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)) localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
} }
override fun get(url: String): PublicSettings { override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url") val settingsStr = localRepository.get("$SETTINGS_KEY$url")
return if (settings == null) hashMapOf() else adapter.fromJson(settings) ?: hashMapOf() 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
...@@ -4,6 +4,7 @@ import android.content.Intent ...@@ -4,6 +4,7 @@ import android.content.Intent
import chat.rocket.android.authentication.ui.newServerIntent import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.ChangeServerActivity import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.server.ui.INTENT_CHAT_ROOM_ID
class ChangeServerNavigator (internal val activity: ChangeServerActivity) { class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
fun toServerScreen() { fun toServerScreen() {
...@@ -11,8 +12,10 @@ class ChangeServerNavigator (internal val activity: ChangeServerActivity) { ...@@ -11,8 +12,10 @@ class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
activity.finish() activity.finish()
} }
fun toChatRooms() { fun toChatRooms(chatRoomId: String? = null) {
activity.startActivity(Intent(activity, MainActivity::class.java)) activity.startActivity(Intent(activity, MainActivity::class.java).also {
it.putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
})
activity.finish() activity.finish()
} }
......
...@@ -21,7 +21,7 @@ class ChangeServerPresenter @Inject constructor( ...@@ -21,7 +21,7 @@ class ChangeServerPresenter @Inject constructor(
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val connectionManager: ConnectionManagerFactory private val connectionManager: ConnectionManagerFactory
) { ) {
fun loadServer(newUrl: String?) { fun loadServer(newUrl: String?, chatRoomId: String? = null) {
launchUI(strategy) { launchUI(strategy) {
view.showProgress() view.showProgress()
var url = newUrl var url = newUrl
...@@ -56,7 +56,7 @@ class ChangeServerPresenter @Inject constructor( ...@@ -56,7 +56,7 @@ class ChangeServerPresenter @Inject constructor(
saveCurrentServerInteractor.save(serverUrl) saveCurrentServerInteractor.save(serverUrl)
view.hideProgress() view.hideProgress()
navigator.toChatRooms() navigator.toChatRooms(chatRoomId)
}.ifNull { }.ifNull {
view.hideProgress() view.hideProgress()
navigator.toServerScreen() navigator.toServerScreen()
......
...@@ -21,7 +21,8 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView { ...@@ -21,7 +21,8 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL) val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL)
presenter.loadServer(serverUrl) val chatRoomId: String? = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
presenter.loadServer(serverUrl, chatRoomId)
} }
override fun showInvalidCredentials() { override fun showInvalidCredentials() {
...@@ -40,11 +41,13 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView { ...@@ -40,11 +41,13 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
private const val INTENT_SERVER_URL = "INTENT_SERVER_URL" 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_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE" private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
const val INTENT_CHAT_ROOM_ID = "INTENT_CHAT_ROOM_ID"
fun Context.changeServerIntent(serverUrl: String? = null): Intent { fun Context.changeServerIntent(serverUrl: String? = null, chatRoomId: String? = ""): Intent {
return Intent(this, ChangeServerActivity::class.java).apply { return Intent(this, ChangeServerActivity::class.java).apply {
serverUrl?.let { url -> serverUrl?.let { url ->
putExtra(INTENT_SERVER_URL, url) putExtra(INTENT_SERVER_URL, url)
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
} }
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
......
...@@ -9,12 +9,10 @@ import android.view.View ...@@ -9,12 +9,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.settings.about.ui.AboutActivity import chat.rocket.android.settings.about.ui.AboutActivity
import chat.rocket.android.settings.password.ui.PasswordActivity import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.settings.presentation.SettingsView import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_settings.* import kotlinx.android.synthetic.main.fragment_settings.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
...@@ -47,7 +45,7 @@ class SettingsFragment: Fragment(), SettingsView, AdapterView.OnItemClickListene ...@@ -47,7 +45,7 @@ class SettingsFragment: Fragment(), SettingsView, AdapterView.OnItemClickListene
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as MainActivity).toolbar.title = getString(R.string.title_settings) (activity as AppCompatActivity?)?.supportActionBar?.title = getString(R.string.title_settings)
} }
private fun startNewActivity(classType: KClass<out AppCompatActivity>) { private fun startNewActivity(classType: KClass<out AppCompatActivity>) {
......
...@@ -40,8 +40,11 @@ fun String.safeUrl(): String { ...@@ -40,8 +40,11 @@ fun String.safeUrl(): String {
fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon" fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon"
fun String.casUrl(serverUrl: String, token: String) = fun String.casUrl(serverUrl: String, casToken: String) =
"${removeTrailingSlash()}?service=${serverUrl.removeTrailingSlash()}/_cas/$token" "${removeTrailingSlash()}?service=${serverUrl.removeTrailingSlash()}/_cas/$casToken"
fun String.samlUrl(provider: String, samlToken: String) =
"${removeTrailingSlash()}/_saml/authorize/$provider/$samlToken"
fun String.termsOfServiceUrl() = "${removeTrailingSlash()}/terms-of-service" fun String.termsOfServiceUrl() = "${removeTrailingSlash()}/terms-of-service"
......
...@@ -31,11 +31,16 @@ fun View.isVisible(): Boolean { ...@@ -31,11 +31,16 @@ fun View.isVisible(): Boolean {
fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View = fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(resource, this, attachToRoot) 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() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
.replace(layoutId, fragment, tag) .replace(layoutId, fragment, tag)
.commit() if (allowStateLoss) {
transaction.commitAllowingStateLoss()
} else {
transaction.commit()
}
} }
fun AppCompatActivity.addFragmentBackStack( fun AppCompatActivity.addFragmentBackStack(
......
...@@ -5,12 +5,17 @@ import androidx.browser.customtabs.CustomTabsIntent ...@@ -5,12 +5,17 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
import timber.log.Timber
fun View.openTabbedUrl(url: Uri) { fun View.openTabbedUrl(url: Uri) {
with(this) { with(this) {
val tabsbuilder = CustomTabsIntent.Builder() val tabsbuilder = CustomTabsIntent.Builder()
tabsbuilder.setToolbarColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme)) tabsbuilder.setToolbarColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
val customTabsIntent = tabsbuilder.build() val customTabsIntent = tabsbuilder.build()
customTabsIntent.launchUrl(context, url) try {
customTabsIntent.launchUrl(context, url)
} catch (ex: Exception) {
Timber.d(ex, "Unable to launch URL")
}
} }
} }
\ No newline at end of file
...@@ -6,6 +6,7 @@ import android.content.Context ...@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.core.net.toUri import androidx.core.net.toUri
...@@ -36,16 +37,18 @@ class OauthWebViewActivity : AppCompatActivity() { ...@@ -36,16 +37,18 @@ class OauthWebViewActivity : AppCompatActivity() {
private lateinit var state: String private lateinit var state: String
private var isWebViewSetUp: Boolean = false private var isWebViewSetUp: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view) setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL) webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" } requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
state = intent.getStringExtra(INTENT_STATE) state = intent.getStringExtra(INTENT_STATE)
requireNotNull(state) { "no state provided in Intent extras" } requireNotNull(state) { "no state provided in Intent extras" }
// Ensures that the cookies is always removed when opening the webview.
CookieManager.getInstance().removeAllCookies(null)
setupToolbar() setupToolbar()
} }
......
package chat.rocket.android.webview.cas.ui package chat.rocket.android.webview.sso.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
...@@ -7,25 +7,30 @@ import android.content.Intent ...@@ -7,25 +7,30 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import chat.rocket.android.R import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.* import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent { fun Context.ssoWebViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply { return Intent(this, SsoWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl) putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken) putExtra(INTENT_SSO_TOKEN, casToken)
} }
} }
private const val INTENT_WEB_PAGE_URL = "web_page_url" private const val INTENT_WEB_PAGE_URL = "web_page_url"
const val INTENT_CAS_TOKEN = "cas_token" const val INTENT_SSO_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() { /**
* This class is responsible to handle the authentication thought single sign-on protocol (CAS and SAML).
*/
class SsoWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String private lateinit var webPageUrl: String
private lateinit var casToken: String private lateinit var casToken: String
private var isWebViewSetUp: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -34,15 +39,20 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -34,15 +39,20 @@ class CasWebViewActivity : AppCompatActivity() {
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL) webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" } requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
casToken = intent.getStringExtra(INTENT_CAS_TOKEN) casToken = intent.getStringExtra(INTENT_SSO_TOKEN)
requireNotNull(casToken) { "no cas_token provided in Intent extras" } requireNotNull(casToken) { "no cas_token provided in Intent extras" }
// Ensures that the cookies is always removed when opening the webview.
CookieManager.getInstance().removeAllCookies(null)
setupToolbar() setupToolbar()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setupWebView() if (!isWebViewSetUp) {
setupWebView()
isWebViewSetUp = true
}
} }
override fun onBackPressed() { override fun onBackPressed() {
...@@ -64,15 +74,16 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -64,15 +74,16 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() { web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user may have already been logged in the CAS, so check if the URL contains the "ticket" word // The user may have already been logged in the SSO, so check if the URL contains
// (that means the user is successful authenticated and we don't need to wait until the page is fully loaded). // the "ticket" or "validate" word (that means the user is successful authenticated
if (url.contains("ticket")) { // and we don't need to wait until the page is fully loaded).
if (url.contains("ticket") || url.contains("validate")) {
closeView(Activity.RESULT_OK) closeView(Activity.RESULT_OK)
} }
} }
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) { if (url.contains("ticket") || url.contains("validate")) {
closeView(Activity.RESULT_OK) closeView(Activity.RESULT_OK)
} else { } else {
view_loading.hide() view_loading.hide()
...@@ -83,7 +94,7 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -83,7 +94,7 @@ class CasWebViewActivity : AppCompatActivity() {
} }
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) { private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) {
setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken)) setResult(activityResult, Intent().putExtra(INTENT_SSO_TOKEN, casToken))
finish() finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down) overridePendingTransition(R.anim.hold, R.anim.slide_down)
} }
......
...@@ -32,6 +32,18 @@ ...@@ -32,6 +32,18 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_headline" /> app:layout_constraintTop_toBottomOf="@+id/text_headline" />
<ImageView
android:id="@+id/image_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_vpn_key_black_24dp"
android:tint="@color/colorDrawableTintGrey"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/text_username_or_email"
app:layout_constraintEnd_toEndOf="@+id/text_username_or_email"
app:layout_constraintTop_toTopOf="@+id/text_username_or_email" />
<EditText <EditText
android:id="@+id/text_password" android:id="@+id/text_password"
style="@style/Authentication.EditText" style="@style/Authentication.EditText"
......
...@@ -7,19 +7,6 @@ ...@@ -7,19 +7,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".chatroom.ui.ChatRoomFragment"> tools:context=".chatroom.ui.ChatRoomFragment">
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/message_list_container" android:id="@+id/message_list_container"
android:layout_width="0dp" android:layout_width="0dp"
...@@ -43,13 +30,11 @@ ...@@ -43,13 +30,11 @@
android:layout_height="100dp" android:layout_height="100dp"
android:src="@drawable/ic_chat_black_24dp" android:src="@drawable/ic_chat_black_24dp"
android:tint="@color/icon_grey" android:tint="@color/icon_grey"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/text_chat_title" app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed" />
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_chat_title" android:id="@+id/text_chat_title"
...@@ -60,12 +45,10 @@ ...@@ -60,12 +45,10 @@
android:textColor="@color/colorSecondaryText" android:textColor="@color/colorSecondaryText"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/text_chat_description" app:layout_constraintBottom_toTopOf="@id/text_chat_description"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon" app:layout_constraintTop_toBottomOf="@id/image_chat_icon" />
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_chat_description" android:id="@+id/text_chat_description"
...@@ -76,11 +59,17 @@ ...@@ -76,11 +59,17 @@
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/colorSecondaryTextLight" android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title" app:layout_constraintTop_toBottomOf="@id/text_chat_title" />
<android.support.constraint.Group
android:id="@+id/empty_chat_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="image_chat_icon, text_chat_title, text_chat_description"
android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView <chat.rocket.android.widget.autocompletion.ui.SuggestionsView
...@@ -142,4 +131,17 @@ ...@@ -142,4 +131,17 @@
tools:text="connected" tools:text="connected"
tools:visibility="visible" /> tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".files.ui.FilesFragment"> tools:context=".files.ui.FilesFragment">
......
...@@ -123,6 +123,7 @@ ...@@ -123,6 +123,7 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string> <string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No se han encontrado resultados</string> <string name="msg_no_search_found">No se han encontrado resultados</string>
<string name="msg_message_copied">Mensaje copiado</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nombre de la sala cambiado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nombre de la sala cambiado para: %1$s por %2$s</string>
......
...@@ -122,6 +122,7 @@ ...@@ -122,6 +122,7 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string> <string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">Aucun résultat trouvé</string> <string name="msg_no_search_found">Aucun résultat trouvé</string>
<string name="msg_message_copied">Message copié</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Le nom de le salle a changé à: %1$s par %2$s</string> <string name="message_room_name_changed">Le nom de le salle a changé à: %1$s par %2$s</string>
......
...@@ -124,6 +124,7 @@ ...@@ -124,6 +124,7 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string> <string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">कोई परिणाम नहीं मिला</string> <string name="msg_no_search_found">कोई परिणाम नहीं मिला</string>
<string name="msg_message_copied">संदेश कॉपी किया गया</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string> <string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string>
......
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
<string name="msg_are_typing">\u0020estão digitando…</string> <string name="msg_are_typing">\u0020estão digitando…</string>
<string name="msg_several_users_are_typing">Vários usuários estão digitando…</string> <string name="msg_several_users_are_typing">Vários usuários estão digitando…</string>
<string name="msg_no_search_found">nenhum resultado encontrado</string> <string name="msg_no_search_found">nenhum resultado encontrado</string>
<string name="msg_message_copied">Mensagem copiada</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
......
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- This is the Cordova GCM sender id-->
<string name="gcm_sender_id" translatable="false">673693445664</string>
</resources>
\ No newline at end of file
...@@ -114,6 +114,7 @@ ...@@ -114,6 +114,7 @@
<string name="msg_are_typing">\u0020are typing…</string> <string name="msg_are_typing">\u0020are typing…</string>
<string name="msg_several_users_are_typing">Several users are typing…</string> <string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No result found</string> <string name="msg_no_search_found">No result found</string>
<string name="msg_message_copied">Message copied</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string> <string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
......
...@@ -10,7 +10,7 @@ buildscript { ...@@ -10,7 +10,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-alpha16' classpath 'com.android.tools.build:gradle:3.2.0-alpha17'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:3.2.0'
......
File added
...@@ -18,7 +18,8 @@ ext { ...@@ -18,7 +18,8 @@ ext {
androidKtx : '1.0.0-alpha1', androidKtx : '1.0.0-alpha1',
dagger : '2.16', dagger : '2.16',
exoPlayer : '2.6.0', exoPlayer : '2.6.0',
playServices : '11.8.0', playServices : '15.0.0',
firebase : '15.0.0',
room : '2.0.0-alpha1', room : '2.0.0-alpha1',
lifecycle : '2.0.0-alpha1', lifecycle : '2.0.0-alpha1',
rxKotlin : '2.2.0', rxKotlin : '2.2.0',
...@@ -32,7 +33,6 @@ ext { ...@@ -32,7 +33,6 @@ ext {
kotshi : '1.0.2', kotshi : '1.0.2',
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.0.3', markwon : '1.0.3',
//sheetMenu : '1.3.3',
sheetMenu : '5ff79ccf14', sheetMenu : '5ff79ccf14',
aVLoadingIndicatorView: '2.1.3', aVLoadingIndicatorView: '2.1.3',
flexbox : '0.3.2', flexbox : '0.3.2',
...@@ -67,7 +67,7 @@ ext { ...@@ -67,7 +67,7 @@ ext {
daggerSupport : "com.google.dagger:dagger-android-support:${versions.dagger}", daggerSupport : "com.google.dagger:dagger-android-support:${versions.dagger}",
daggerProcessor : "com.google.dagger:dagger-compiler:${versions.dagger}", daggerProcessor : "com.google.dagger:dagger-compiler:${versions.dagger}",
daggerAndroidApt : "com.google.dagger:dagger-android-processor:${versions.dagger}", daggerAndroidApt : "com.google.dagger:dagger-android-processor:${versions.dagger}",
playServicesGcm : "com.google.android.gms:play-services-gcm:${versions.playServices}", fcm : "com.google.firebase:firebase-messaging:${versions.firebase}",
playServicesAuth : "com.google.android.gms:play-services-auth:${versions.playServices}", playServicesAuth : "com.google.android.gms:play-services-auth:${versions.playServices}",
exoPlayer : "com.google.android.exoplayer:exoplayer:${versions.exoPlayer}", exoPlayer : "com.google.android.exoplayer:exoplayer:${versions.exoPlayer}",
...@@ -106,12 +106,6 @@ ext { ...@@ -106,12 +106,6 @@ ext {
aVLoadingIndicatorView: "com.wang.avi:library:${versions.aVLoadingIndicatorView}", aVLoadingIndicatorView: "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
// For testing
junit : "junit:junit:$versions.junit",
espressoCore : "androidx.test.espresso:espresso-core:${versions.espresso}",
espressoIntents : "androidx.test.espresso:espresso-intents:${versions.espresso}",
roomTest : "android.arch.persistence.room:testing:${versions.room}",
truth : "com.google.truth:truth:$versions.truth",
//For the wear app //For the wear app
wearable : "com.google.android.support:wearable:${versions.wear}", wearable : "com.google.android.support:wearable:${versions.wear}",
...@@ -119,6 +113,13 @@ ext { ...@@ -119,6 +113,13 @@ ext {
percentLayout : "com.android.support:percent:${versions.supportWearable}", percentLayout : "com.android.support:percent:${versions.supportWearable}",
supportWearable : "com.android.support:support-v4:${versions.supportWearable}", supportWearable : "com.android.support:support-v4:${versions.supportWearable}",
wearableRecyclerView : "com.android.support:recyclerview-v7:${versions.supportWearable}", wearableRecyclerView : "com.android.support:recyclerview-v7:${versions.supportWearable}",
wearSupport : "com.android.support:wear:${versions.supportWearable}" wearSupport : "com.android.support:wear:${versions.supportWearable}",
// For testing
junit : "junit:junit:$versions.junit",
espressoCore : "androidx.test.espresso:espresso-core:${versions.espresso}",
espressoIntents : "androidx.test.espresso:espresso-intents:${versions.espresso}",
roomTest : "android.arch.persistence.room:testing:${versions.room}",
truth : "com.google.truth:truth:$versions.truth"
] ]
} }
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