Commit e5d8c11a authored by Leonardo Aramaki's avatar Leonardo Aramaki

Merge branch 'develop-2.x' into perf-and-fixes

parents a992e747 40fc6074
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
...@@ -54,6 +54,11 @@ ...@@ -54,6 +54,11 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
......
...@@ -37,7 +37,7 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -37,7 +37,7 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings. * Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
* *
* REMARK: We must set up the CAS button listener [setupCasButtonListener]. * REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/ */
fun showCasButton() fun showCasButton()
...@@ -49,8 +49,8 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -49,8 +49,8 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Setups the CAS button when tapped. * Setups the CAS button when tapped.
* *
* @param casUrl The CAS URL to login/sign up with. * @param casUrl The CAS URL to authenticate with.
* @param casToken The requested Token sent to the CAS server. * @param casToken The requested token to be sent to the CAS server.
*/ */
fun setupCasButtonListener(casUrl: String, casToken: String) fun setupCasButtonListener(casUrl: String, casToken: String)
...@@ -96,40 +96,80 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -96,40 +96,80 @@ interface LoginView : LoadingView, MessageView, InternetView {
fun hideLoginButton() fun hideLoginButton()
/** /**
* Shows the "login by Facebook view if it is enabled by the server settings. * Shows the "login by Facebook view if it is enable by the server settings.
*/ */
fun enableLoginByFacebook() fun enableLoginByFacebook()
/** /**
* Shows the "login by Github" view if it is enabled by the server settings. * Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/ */
fun enableLoginByGithub() fun enableLoginByGithub()
/** /**
* Shows the "login by Google" view if it is enabled by the server settings. * Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it [setupGoogleButtonListener].
*/ */
fun enableLoginByGoogle() fun enableLoginByGoogle()
/** /**
* Shows the "login by Linkedin" view if it is enabled by the server settings. * Setups the Google button when tapped.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enable by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it [setupLinkedinButtonListener].
*/ */
fun enableLoginByLinkedin() fun enableLoginByLinkedin()
/** /**
* Shows the "login by Meteor" view if it is enabled by the server settings. * Setups the Linkedin button when tapped.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/ */
fun enableLoginByMeteor() fun enableLoginByMeteor()
/** /**
* Shows the "login by Twitter" view if it is enabled by the server settings. * Shows the "login by Twitter" view if it is enable by the server settings.
*/ */
fun enableLoginByTwitter() fun enableLoginByTwitter()
/** /**
* Shows the "login by Gitlab" view if it is enabled by the server settings. * Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/ */
fun enableLoginByGitlab() fun enableLoginByGitlab()
/**
* Setups the Gitlab button when tapped.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/** /**
* 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)).
*/ */
......
...@@ -19,12 +19,17 @@ import chat.rocket.android.authentication.login.presentation.LoginView ...@@ -19,12 +19,17 @@ import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
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 chat.rocket.android.webview.cas.ui.webViewIntent import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN
import chat.rocket.android.webview.cas.ui.casWebViewIntent
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1 internal const val REQUEST_CODE_FOR_CAS = 1
internal const val REQUEST_CODE_FOR_OAUTH = 2
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter @Inject lateinit var presenter: LoginPresenter
...@@ -64,10 +69,14 @@ class LoginFragment : Fragment(), LoginView { ...@@ -64,10 +69,14 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
data?.apply {
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
}
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) {
data?.apply { data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token")) presenter.authenticateWithOauth(getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN), getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET))
} }
} }
} }
...@@ -121,7 +130,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -121,7 +130,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupLoginButtonListener() { override fun setupLoginButtonListener() {
button_log_in.setOnClickListener { button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent) presenter.authenticateWithUserAndPassword(text_username_or_email.textContent, text_password.textContent)
} }
} }
...@@ -147,7 +156,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -147,7 +156,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupCasButtonListener(casUrl: String, casToken: String) { override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener { button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS) startActivityForResult(context?.casWebViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
} }
...@@ -192,31 +201,60 @@ class LoginFragment : Fragment(), LoginView { ...@@ -192,31 +201,60 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun enableLoginByFacebook() { override fun enableLoginByFacebook() {
button_facebook.isEnabled = true button_facebook.isClickable = true
} }
override fun enableLoginByGithub() { override fun enableLoginByGithub() {
button_github.isEnabled = true button_github.isClickable = true
}
override fun setupGithubButtonListener(githubUrl: String, state: String) {
button_github.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(githubUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByGoogle() { override fun enableLoginByGoogle() {
button_google.isEnabled = true button_google.isClickable = true
}
// TODO: Use custom tabs instead of web view. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
override fun setupGoogleButtonListener(googleUrl: String, state: String) {
button_google.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(googleUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByLinkedin() { override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true button_linkedin.isClickable = true
}
override fun setupLinkedinButtonListener(linkedinUrl: String, state: String) {
button_linkedin.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(linkedinUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByMeteor() { override fun enableLoginByMeteor() {
button_meteor.isEnabled = true button_meteor.isClickable = true
} }
override fun enableLoginByTwitter() { override fun enableLoginByTwitter() {
button_twitter.isEnabled = true button_twitter.isClickable = true
} }
override fun enableLoginByGitlab() { override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true button_gitlab.isClickable = true
}
override fun setupGitlabButtonListener(gitlabUrl: String, state: String) {
button_gitlab.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(gitlabUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun setupFabListener() { override fun setupFabListener() {
...@@ -253,8 +291,8 @@ class LoginFragment : Fragment(), LoginView { ...@@ -253,8 +291,8 @@ class LoginFragment : Fragment(), LoginView {
social_accounts_container.postDelayed({ social_accounts_container.postDelayed({
(0..social_accounts_container.childCount) (0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton } .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled } .filter { it.isClickable }
.forEach { it.visibility = View.VISIBLE } .forEach { it.setVisible(true)}
}, 1000) }, 1000)
} }
...@@ -284,13 +322,10 @@ class LoginFragment : Fragment(), LoginView { ...@@ -284,13 +322,10 @@ class LoginFragment : Fragment(), LoginView {
} }
private fun showThreeSocialAccountsMethods() { private fun showThreeSocialAccountsMethods() {
var count = 0 (0..social_accounts_container.childCount)
for (i in 0..social_accounts_container.childCount) { .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue .filter { it.isClickable }
if (view.isEnabled && count < 3) { .take(3)
view.visibility = View.VISIBLE .forEach { it.setVisible(true) }
count++
}
}
} }
} }
\ No newline at end of file
...@@ -10,6 +10,7 @@ import chat.rocket.android.server.domain.* ...@@ -10,6 +10,7 @@ import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -27,6 +28,7 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -27,6 +28,7 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) { settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
...@@ -98,9 +100,10 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -98,9 +100,10 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let { localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it) client.registerPushToken(it, getAccountsInteractor.get(), factory)
} }
// TODO: Schedule push token registering when it comes up null // TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
} }
private suspend fun saveAccount(me: Myself) { private suspend fun saveAccount(me: Myself) {
......
...@@ -6,5 +6,5 @@ const val PEOPLE = 0 ...@@ -6,5 +6,5 @@ const val PEOPLE = 0
const val ROOMS = 1 const val ROOMS = 1
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS]) @IntDef(PEOPLE, ROOMS)
annotation class AutoCompleteType annotation class AutoCompleteType
...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer) private val manager = factory.create(currentServer)
private val client = manager.client private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>() private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null private var chatRoomId: String? = null
......
...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!) private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0 private var pinnedMessagesListOffset: Int = 0
/** /**
......
...@@ -106,23 +106,52 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -106,23 +106,52 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> { private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map { return users.map {
ChatRoom(it.id, RoomType.DIRECT_MESSAGE, SimpleUser( ChatRoom(id = it.id,
username = it.username, name = it.name, id = null), it.name ?: "", type = RoomType.DIRECT_MESSAGE,
it.name, false, null, null, null, user = SimpleUser(username = it.username, name = it.name, id = null),
null, null, null, false, false, name = it.name ?: "",
false, 0L, 0L, null, fullName = it.name,
null, client readonly = false,
updatedAt = null,
timestamp = null,
lastSeen = null,
topic = null,
description = null,
announcement = null,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = null,
client = client
) )
} }
} }
private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> { private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map { return rooms.map {
ChatRoom(it.id, it.type, it.user, it.name ?: "", ChatRoom(id = it.id,
it.fullName, it.readonly, it.updatedAt, null, null, type = it.type,
it.topic, it.announcement, null, false, false, user = it.user,
false, 0L, 0L, 0L, it.lastMessage, name = it.name ?: "",
client fullName = it.fullName,
readonly = it.readonly,
updatedAt = it.updatedAt,
timestamp = null,
lastSeen = null,
topic = it.topic,
description = it.description,
announcement = it.announcement,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = it.lastMessage,
client = client
) )
} }
} }
......
...@@ -162,7 +162,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -162,7 +162,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this, recycler_view.adapter = ChatRoomsAdapter(
this,
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom -> settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom ->
presenter.loadChatRoom(chatRoom) presenter.loadChatRoom(chatRoom)
} }
......
...@@ -35,6 +35,49 @@ object UrlHelper { ...@@ -35,6 +35,49 @@ object UrlHelper {
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String = fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the Github Oauth URL.
*
* @param clientId The GitHub client ID.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Github Oauth URL.
*/
fun getGithubOauthUrl(clientId: String, state: String): String =
"https://github.com/login/oauth/authorize?scope=user:email&client_id=$clientId&state=$state"
/**
* Returns the Google Oauth URL.
*
* @param clientId The Google client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Google Oauth URL.
*/
fun getGoogleOauthUrl(clientId: String, serverUrl: String, state: String) =
"https://accounts.google.com/o/oauth2/v2/auth?client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/google?close&response_type=code&state=$state&scope=email%20profile"
/**
* Returns the Linkedin Oauth URL.
*
* @param clientId The Linkedin client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Linkedin Oauth URL.
*/
fun getLinkedinOauthUrl(clientId: String, serverUrl: String, state: String) =
"https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/linkedin?close&state=$state"
/**
* Returns the Gitlab Oauth URL.
*
* @param clientId The Gitlab client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Gitlab Oauth URL.
*/
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String =
"https://gitlab.com/oauth/authorize?client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/gitlab?close&response_type=code&state=$state&scope=read_user"
/** /**
* Returns the server's Terms of Service URL. * Returns the server's Terms of Service URL.
* *
......
...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions ...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.util.Base64
import android.util.Patterns import android.util.Patterns
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.json.JSONObject
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import java.net.URLDecoder
import java.security.SecureRandom import java.security.SecureRandom
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
...@@ -33,7 +36,23 @@ fun EditText.erase() { ...@@ -33,7 +36,23 @@ fun EditText.erase() {
} }
} }
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String {
return Base64.encodeToString(this.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
}
fun String.decodeFromBase64(): String {
return Base64.decode(this, Base64.DEFAULT).toString(charset("UTF-8"))
}
fun String.decodeUrl(): String {
return URLDecoder.decode(this, "UTF-8")
}
fun String.toJsonObject(): JSONObject {
return JSONObject(this)
}
fun generateRandomString(stringLength: Int): String { fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
......
...@@ -13,7 +13,7 @@ import chat.rocket.android.R ...@@ -13,7 +13,7 @@ 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.webViewIntent(webPageUrl: String, casToken: String): Intent { fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply { return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl) putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken) putExtra(INTENT_CAS_TOKEN, casToken)
...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent { ...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
} }
private const val INTENT_WEB_PAGE_URL = "web_page_url" private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token" const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() { class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String private lateinit var webPageUrl: String
...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() {
if (web_view.canGoBack()) { if (web_view.canGoBack()) {
web_view.goBack() web_view.goBack()
} else { } else {
finishActivity(false) closeView()
} }
} }
private fun setupToolbar() { private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication) toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) } toolbar.setNavigationOnClickListener { closeView() }
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
...@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -64,16 +64,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 can be already logged in the CAS, so check if the URL contains the "ticket" word // The user may have already been logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished. // (that means the user is successful authenticated and we don't need to wait until the page is fully loaded).
if (url.contains("ticket")) { if (url.contains("ticket")) {
finishActivity(true) 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")) {
finishActivity(true) closeView(Activity.RESULT_OK)
} else { } else {
view_loading.hide() view_loading.hide()
} }
...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.loadUrl(webPageUrl) web_view.loadUrl(webPageUrl)
} }
private fun finishActivity(setResultOk: Boolean) { private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) {
if (setResultOk) { setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish() finish()
} else {
super.onBackPressed()
}
overridePendingTransition(R.anim.hold, R.anim.slide_down) overridePendingTransition(R.anim.hold, R.anim.slide_down)
} }
} }
\ No newline at end of file
package chat.rocket.android.webview.oauth.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.net.toUri
import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
import org.json.JSONObject
fun Context.oauthWebViewIntent(webPageUrl: String, state: String): Intent {
return Intent(this, OauthWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_STATE, state)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_STATE = "state"
private const val JSON_CREDENTIAL_TOKEN = "credentialToken"
private const val JSON_CREDENTIAL_SECRET = "credentialSecret"
const val INTENT_OAUTH_CREDENTIAL_TOKEN = "credential_token"
const val INTENT_OAUTH_CREDENTIAL_SECRET = "credential_secret"
// Shows a WebView to the user authenticate with your Gitlab credentials.
class OauthWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var state: String
private var isWebViewSetUp: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
state = intent.getStringExtra(INTENT_STATE)
requireNotNull(state) { "no state provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
if (!isWebViewSetUp) {
setupWebView()
isWebViewSetUp = true
}
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
closeView()
}
}
private fun setupToolbar() {
with(toolbar) {
title = getString(R.string.title_authentication)
setNavigationIcon(R.drawable.ic_close_white_24dp)
setNavigationOnClickListener { closeView() }
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
with(web_view.settings) {
javaScriptEnabled = true
// TODO This is required to make Google OAuth work, but we shoud use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19"
}
web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (url.contains(JSON_CREDENTIAL_TOKEN) && url.contains(JSON_CREDENTIAL_SECRET)) {
if (isStateValid(url)) {
val jsonResult = url.decodeUrl()
.substringAfter("#")
.toJsonObject()
val credentialToken = getCredentialToken(jsonResult)
val credentialSecret = getCredentialSecret(jsonResult)
if (credentialToken.isNotEmpty() && credentialSecret.isNotEmpty()) {
closeView(Activity.RESULT_OK, credentialToken, credentialSecret)
}
}
}
view_loading.hide()
}
}
web_view.loadUrl(webPageUrl)
}
// If the states matches, then try to get the code, otherwise the request was created by a third party and the process should be aborted.
private fun isStateValid(url: String): Boolean =
url.substringBefore("#").toUri().getQueryParameter(INTENT_STATE) == state
private fun getCredentialToken(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_TOKEN)
private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) {
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret))
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_facebook" android:contentDescription="@string/msg_content_description_log_in_using_facebook"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_facebook" android:src="@drawable/ic_facebook"
...@@ -124,6 +125,7 @@ ...@@ -124,6 +125,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_github" android:contentDescription="@string/msg_content_description_log_in_using_github"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_github" android:src="@drawable/ic_github"
...@@ -135,6 +137,7 @@ ...@@ -135,6 +137,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_google" android:contentDescription="@string/msg_content_description_log_in_using_google"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:src="@drawable/ic_google" android:src="@drawable/ic_google"
...@@ -146,6 +149,7 @@ ...@@ -146,6 +149,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_linkedin" android:contentDescription="@string/msg_content_description_log_in_using_linkedin"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_linkedin" android:src="@drawable/ic_linkedin"
...@@ -157,6 +161,7 @@ ...@@ -157,6 +161,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_meteor" android:contentDescription="@string/msg_content_description_log_in_using_meteor"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_meteor" android:src="@drawable/ic_meteor"
...@@ -168,6 +173,7 @@ ...@@ -168,6 +173,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_twitter" android:contentDescription="@string/msg_content_description_log_in_using_twitter"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_twitter" android:src="@drawable/ic_twitter"
...@@ -179,6 +185,7 @@ ...@@ -179,6 +185,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_gitlab" android:contentDescription="@string/msg_content_description_log_in_using_gitlab"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_gitlab" android:src="@drawable/ic_gitlab"
......
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
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