Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt

1260 lines
54 KiB
Kotlin
Raw Normal View History

2016-07-07 09:39:32 +02:00
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.activity
2016-12-03 06:48:40 +01:00
import android.accounts.Account
2016-12-02 05:36:50 +01:00
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
2016-07-07 09:39:32 +02:00
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri
import android.os.AsyncTask
2017-10-01 12:01:41 +02:00
import android.os.Build
2016-07-07 09:39:32 +02:00
import android.os.Bundle
2020-01-26 08:35:15 +01:00
import androidx.loader.app.LoaderManager
import androidx.core.content.ContextCompat
import androidx.loader.content.Loader
import androidx.collection.ArraySet
import androidx.core.view.ViewCompat
import androidx.appcompat.app.AlertDialog
2016-07-07 09:39:32 +02:00
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
2017-04-18 11:56:59 +02:00
import android.view.*
2016-07-07 09:39:32 +02:00
import android.view.View.OnClickListener
2017-04-23 15:02:04 +02:00
import android.webkit.CookieManager
2017-05-02 04:14:00 +02:00
import android.webkit.CookieSyncManager
2017-06-19 15:45:41 +02:00
import android.widget.BaseExpandableListAdapter
import android.widget.ExpandableListView
import android.widget.TextView
import android.widget.Toast
2016-07-07 09:39:32 +02:00
import kotlinx.android.synthetic.main.activity_sign_in.*
2017-06-19 15:45:41 +02:00
import kotlinx.android.synthetic.main.dialog_expandable_list.*
import kotlinx.android.synthetic.main.dialog_login_verification_code.*
2017-08-28 11:50:04 +02:00
import nl.komponents.kovenant.CancelException
import nl.komponents.kovenant.Deferred
2017-03-22 13:00:29 +01:00
import nl.komponents.kovenant.combine.and
2017-08-28 11:50:04 +02:00
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi
2017-04-18 15:19:07 +02:00
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.kpreferences.get
2017-01-02 09:33:27 +01:00
import org.mariotaku.ktextension.*
2016-07-07 09:39:32 +02:00
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
2017-04-18 15:19:07 +02:00
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.mastodon.MastodonOAuth2
import org.mariotaku.microblog.library.mastodon.annotation.AuthScope
2016-07-07 09:39:32 +02:00
import org.mariotaku.microblog.library.twitter.TwitterOAuth
import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization
import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization
import org.mariotaku.microblog.library.twitter.model.Paging
2016-07-07 09:39:32 +02:00
import org.mariotaku.microblog.library.twitter.model.User
import org.mariotaku.restfu.http.Endpoint
import org.mariotaku.restfu.oauth.OAuthToken
import org.mariotaku.restfu.oauth2.OAuth2Authorization
2016-07-07 09:39:32 +02:00
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.*
2016-12-03 06:48:40 +01:00
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_API_CONFIG
import org.mariotaku.twidere.constant.apiLastChangeKey
import org.mariotaku.twidere.constant.defaultAPIConfigKey
import org.mariotaku.twidere.constant.randomizeAccountNameKey
2017-06-19 15:45:41 +02:00
import org.mariotaku.twidere.extension.*
2017-04-20 14:04:37 +02:00
import org.mariotaku.twidere.extension.model.*
2017-05-03 15:42:05 +02:00
import org.mariotaku.twidere.extension.model.api.isFanfouUser
import org.mariotaku.twidere.extension.model.api.key
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
2017-04-22 16:32:00 +02:00
import org.mariotaku.twidere.extension.model.api.toParcelable
2017-01-25 13:08:23 +01:00
import org.mariotaku.twidere.fragment.APIEditorDialogFragment
2016-07-07 09:39:32 +02:00
import org.mariotaku.twidere.fragment.BaseDialogFragment
import org.mariotaku.twidere.fragment.ProgressDialogFragment
2017-04-18 11:56:59 +02:00
import org.mariotaku.twidere.loader.DefaultAPIConfigLoader
import org.mariotaku.twidere.model.CustomAPIConfig
2016-12-03 06:48:40 +01:00
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.SingleResponse
import org.mariotaku.twidere.model.UserKey
2016-12-15 06:11:32 +01:00
import org.mariotaku.twidere.model.account.AccountExtras
import org.mariotaku.twidere.model.account.MastodonAccountExtras
2016-12-03 06:48:40 +01:00
import org.mariotaku.twidere.model.account.StatusNetAccountExtras
import org.mariotaku.twidere.model.account.TwitterAccountExtras
import org.mariotaku.twidere.model.account.cred.*
2016-12-15 06:11:32 +01:00
import org.mariotaku.twidere.model.analyzer.SignIn
2016-12-04 04:58:03 +01:00
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableUserUtils
2016-07-07 09:39:32 +02:00
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.OAuthPasswordAuthenticator.*
2017-01-07 17:13:11 +01:00
import java.io.IOException
2016-07-07 09:39:32 +02:00
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.ArrayList
2016-07-07 09:39:32 +02:00
2016-12-02 05:36:50 +01:00
2017-04-06 05:55:04 +02:00
class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
APIEditorDialogFragment.APIEditorCallback {
2017-04-18 11:56:59 +02:00
private lateinit var apiConfig: CustomAPIConfig
2016-07-07 09:39:32 +02:00
private var apiChangeTimestamp: Long = 0
private var signInTask: AbstractSignInTask? = null
2016-12-02 05:36:50 +01:00
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
private var accountAuthenticatorResult: Bundle? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
accountAuthenticatorResponse?.onRequestContinued()
setContentView(R.layout.activity_sign_in)
editUsername.addTextChangedListener(this)
editPassword.addTextChangedListener(this)
2016-07-07 09:39:32 +02:00
2016-12-02 05:36:50 +01:00
signIn.setOnClickListener(this)
signUp.setOnClickListener(this)
passwordSignIn.setOnClickListener(this)
val color = ColorStateList.valueOf(ContextCompat.getColor(this,
R.color.material_light_green))
ViewCompat.setBackgroundTintList(signIn, color)
2017-04-18 11:56:59 +02:00
if (savedInstanceState != null) {
2019-10-25 10:50:10 +02:00
apiConfig = savedInstanceState.getParcelable(EXTRA_API_CONFIG)!!
2017-04-18 11:56:59 +02:00
apiChangeTimestamp = savedInstanceState.getLong(EXTRA_API_LAST_CHANGE)
} else {
apiConfig = kPreferences[defaultAPIConfigKey]
}
2016-12-02 05:36:50 +01:00
updateSignInType()
setSignInButton()
if (savedInstanceState == null) {
// Only start at the first time
2017-05-18 16:26:45 +02:00
showLoginTypeChooser()
2017-10-01 12:01:41 +02:00
// Must call this before cookie manager under lollipop
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
@Suppress("DEPRECATION")
CookieSyncManager.createInstance(this)
}
2017-04-23 15:02:04 +02:00
CookieManager.getInstance().removeAllCookiesSupport()
}
2016-07-07 09:39:32 +02:00
}
2016-12-02 05:36:50 +01:00
override fun onCreateOptionsMenu(menu: Menu): Boolean {
2016-12-29 06:50:18 +01:00
super.onCreateOptionsMenu(menu)
2016-12-02 05:36:50 +01:00
menuInflater.inflate(R.menu.menu_sign_in, menu)
return true
2016-07-07 09:39:32 +02:00
}
2016-12-02 05:36:50 +01:00
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
2016-07-07 09:39:32 +02:00
when (requestCode) {
REQUEST_EDIT_API -> {
2017-04-18 11:56:59 +02:00
if (resultCode == Activity.RESULT_OK && data != null) {
apiConfig = data.getParcelableExtra(EXTRA_API_CONFIG)
2016-07-07 09:39:32 +02:00
updateSignInType()
}
setSignInButton()
invalidateOptionsMenu()
}
2017-04-18 15:19:07 +02:00
REQUEST_BROWSER_TWITTER_SIGN_IN -> {
2016-07-07 09:39:32 +02:00
if (resultCode == Activity.RESULT_OK && data != null) {
2017-01-27 15:35:28 +01:00
handleBrowserLoginResult(data)
2016-07-07 09:39:32 +02:00
}
}
2017-04-18 15:19:07 +02:00
REQUEST_BROWSER_MASTODON_SIGN_IN -> {
if (resultCode == Activity.RESULT_OK && data != null) {
2017-04-18 16:00:16 +02:00
val code = data.getStringExtra(EXTRA_CODE)
2019-10-25 10:50:10 +02:00
val extras = data.getBundleExtra(EXTRA_EXTRAS)!!
val host = extras.getString(EXTRA_HOST)!!
val clientId = extras.getString(EXTRA_CLIENT_ID)!!
val clientSecret = extras.getString(EXTRA_CLIENT_SECRET)!!
2017-04-18 16:00:16 +02:00
2017-04-18 15:19:07 +02:00
finishMastodonBrowserLogin(host, clientId, clientSecret, code)
}
}
2016-07-07 09:39:32 +02:00
}
super.onActivityResult(requestCode, resultCode, data)
}
2016-12-02 05:36:50 +01:00
override fun finish() {
accountAuthenticatorResponse?.let { response ->
// send the result bundle back if set, otherwise send an error.
if (accountAuthenticatorResult != null) {
response.onResult(accountAuthenticatorResult)
} else {
response.onError(AccountManager.ERROR_CODE_CANCELED, "canceled")
}
accountAuthenticatorResponse = null
}
super.finish()
}
override fun afterTextChanged(s: Editable) {
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
2016-07-07 09:39:32 +02:00
override fun onClick(v: View) {
when (v) {
signUp -> {
val uri = apiConfig.signUpUrl?.let(Uri::parse) ?: return
OnLinkClickHandler.openLink(this, preferences, uri)
2016-07-07 09:39:32 +02:00
}
signIn -> {
if (usernamePasswordContainer.visibility != View.VISIBLE) {
editUsername.text = null
editPassword.text = null
}
setDefaultAPI()
2017-04-18 15:19:07 +02:00
if (apiConfig.type == AccountType.MASTODON) {
performMastodonLogin()
} else when (apiConfig.credentialsType) {
2017-04-18 11:56:59 +02:00
Credentials.Type.OAUTH -> {
2017-04-18 15:19:07 +02:00
performBrowserLogin()
2017-04-18 11:56:59 +02:00
}
else -> {
2017-04-18 15:19:07 +02:00
val username = editUsername.text.toString()
val password = editPassword.text.toString()
performUserPassLogin(username, password)
2017-04-18 11:56:59 +02:00
}
2017-01-27 15:35:28 +01:00
}
2016-07-07 09:39:32 +02:00
}
passwordSignIn -> {
2017-01-19 12:43:20 +01:00
executeAfterFragmentResumed { fragment ->
2016-07-07 09:39:32 +02:00
val df = PasswordSignInDialogFragment()
2017-01-19 12:43:20 +01:00
df.show(fragment.supportFragmentManager, "password_sign_in")
2016-07-07 09:39:32 +02:00
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
val accountKeys = DataStoreUtils.getActivatedAccountKeys(this)
2016-12-03 06:48:40 +01:00
if (accountKeys.isNotEmpty()) {
2016-07-07 09:39:32 +02:00
onBackPressed()
}
}
R.id.settings -> {
if (signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING)
return false
2017-03-13 06:35:11 +01:00
startActivity(IntentUtils.settings())
2016-07-07 09:39:32 +02:00
}
R.id.edit_api -> {
if (signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING)
return false
setDefaultAPI()
2017-01-25 13:08:23 +01:00
val df = APIEditorDialogFragment()
df.arguments = Bundle {
this[EXTRA_API_CONFIG] = apiConfig
this[APIEditorDialogFragment.EXTRA_SHOW_LOAD_DEFAULTS] = true
}
df.show(supportFragmentManager, "edit_api_config")
2016-07-07 09:39:32 +02:00
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
super.onPrepareOptionsMenu(menu)
2016-07-07 09:39:32 +02:00
val itemBrowser = menu.findItem(R.id.open_in_browser)
if (itemBrowser != null) {
val is_oauth = apiConfig.credentialsType == Credentials.Type.OAUTH
2016-07-07 09:39:32 +02:00
itemBrowser.isVisible = is_oauth
itemBrowser.isEnabled = is_oauth
}
return true
}
2017-01-25 13:08:23 +01:00
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(EXTRA_API_CONFIG, apiConfig)
2016-07-07 09:39:32 +02:00
outState.putLong(EXTRA_API_LAST_CHANGE, apiChangeTimestamp)
super.onSaveInstanceState(outState)
}
2016-12-02 05:36:50 +01:00
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
2016-07-07 09:39:32 +02:00
setSignInButton()
}
2017-01-25 13:08:23 +01:00
override fun onSaveAPIConfig(config: CustomAPIConfig) {
apiConfig = config
updateSignInType()
setSignInButton()
invalidateOptionsMenu()
}
2017-04-18 15:19:07 +02:00
private fun performBrowserLogin() {
val weakThis = WeakReference(this)
executeAfterFragmentResumed { activity ->
ProgressDialogFragment.show(activity.supportFragmentManager, "get_request_token")
} and task {
val activity = weakThis.get() ?: throw InterruptedException()
val apiConfig = activity.apiConfig
val apiUrlFormat = apiConfig.apiUrlFormat ?:
throw MicroBlogException("Invalid API URL format")
val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat,
apiConfig.isSameOAuthUrl)
val auth = apiConfig.getOAuthAuthorization() ?:
throw MicroBlogException("Invalid OAuth credentials")
val oauth = newMicroBlogInstance(activity, endpoint, auth, apiConfig.type,
TwitterOAuth::class.java)
return@task oauth.getRequestToken(OAUTH_CALLBACK_OOB)
}.successUi { requestToken ->
val activity = weakThis.get() ?: return@successUi
val intent = Intent(activity, BrowserSignInActivity::class.java)
val apiConfig = activity.apiConfig
val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiConfig.apiUrlFormat!!, true)
intent.data = Uri.parse(endpoint.construct("/oauth/authorize", arrayOf("oauth_token",
requestToken.oauthToken)))
intent.putExtra(EXTRA_EXTRAS, Bundle {
this[EXTRA_REQUEST_TOKEN] = requestToken.oauthToken
this[EXTRA_REQUEST_TOKEN_SECRET] = requestToken.oauthTokenSecret
})
activity.startActivityForResult(intent, REQUEST_BROWSER_TWITTER_SIGN_IN)
}.failUi {
val activity = weakThis.get() ?: return@failUi
// TODO show error message
2020-04-14 08:31:33 +02:00
if (it is MicroBlogException) {
Toast.makeText(activity, it.message, Toast.LENGTH_SHORT).show()
}
2017-04-18 15:19:07 +02:00
}.alwaysUi {
executeAfterFragmentResumed {
it.supportFragmentManager.dismissDialogFragment("get_request_token")
}
}
}
private fun performMastodonLogin() {
val weakThis = WeakReference(this)
2017-04-18 16:00:16 +02:00
val host = editUsername.string?.takeIf(String::isNotEmpty) ?: run {
Toast.makeText(this, R.string.message_toast_invalid_mastodon_host,
2017-04-18 15:19:07 +02:00
Toast.LENGTH_SHORT).show()
return
}
val scopes = arrayOf(AuthScope.READ, AuthScope.WRITE, AuthScope.FOLLOW)
executeAfterFragmentResumed { activity ->
ProgressDialogFragment.show(activity.supportFragmentManager, "open_browser_auth")
} and task {
val activity = weakThis.get() ?: throw InterruptedException()
val registry = activity.mastodonApplicationRegistry
return@task Pair(host, registry[host] ?: registry.fetch(host, scopes))
2017-04-18 15:19:07 +02:00
}.successUi { (host, app) ->
val activity = weakThis.get() ?: return@successUi
val endpoint = Endpoint("https://$host/")
val intent = Intent(activity, BrowserSignInActivity::class.java)
intent.data = Uri.parse(endpoint.construct("/oauth/authorize",
arrayOf("response_type", "code"),
arrayOf("client_id", app.clientId),
arrayOf("redirect_uri", MASTODON_CALLBACK_URL),
arrayOf("scope", scopes.joinToString(" "))))
intent.putExtra(EXTRA_EXTRAS, Bundle {
this[EXTRA_HOST] = host
this[EXTRA_CLIENT_ID] = app.clientId
this[EXTRA_CLIENT_SECRET] = app.clientSecret
})
activity.startActivityForResult(intent, REQUEST_BROWSER_MASTODON_SIGN_IN)
}.failUi {
val activity = weakThis.get() ?: return@failUi
// TODO show error message
2017-04-23 10:00:05 +02:00
activity.onSignInError(it)
2017-04-18 15:19:07 +02:00
}.alwaysUi {
2017-04-23 10:00:05 +02:00
val activity = weakThis.get() ?: return@alwaysUi
activity.executeAfterFragmentResumed {
2017-04-18 15:19:07 +02:00
it.supportFragmentManager.dismissDialogFragment("open_browser_auth")
}
}
2017-01-25 13:08:23 +01:00
}
2017-04-18 15:19:07 +02:00
private fun performUserPassLogin(username: String, password: String) {
2016-07-07 09:39:32 +02:00
if (signInTask != null && signInTask!!.status == AsyncTask.Status.RUNNING) {
signInTask!!.cancel(true)
}
2017-05-19 17:08:23 +02:00
signInTask = SignInTask(this, username, password, apiConfig).apply { execute() }
2016-07-07 09:39:32 +02:00
}
private fun onSignInResult(result: SignInResponse) {
2017-01-25 13:08:23 +01:00
val am = AccountManager.get(this)
setSignInButton()
if (result.alreadyLoggedIn) {
result.updateAccount(am)
contentResolver.deleteAccountData(result.user.key)
2017-01-26 14:28:43 +01:00
Toast.makeText(this, R.string.message_toast_already_logged_in, Toast.LENGTH_SHORT).show()
2017-01-25 13:08:23 +01:00
} else {
result.addAccount(am, preferences[randomizeAccountNameKey])
Analyzer.log(SignIn(true, accountType = result.type,
credentialsType = apiConfig.credentialsType,
officialKey = result.extras?.official == true))
2017-01-25 13:08:23 +01:00
finishSignIn()
}
}
private fun dismissDialogFragment(tag: String) {
2017-01-25 13:08:23 +01:00
executeAfterFragmentResumed {
it.supportFragmentManager.dismissDialogFragment(tag)
2017-01-25 13:08:23 +01:00
}
}
internal fun onSignInError(exception: Exception) {
2017-01-26 16:15:05 +01:00
DebugLog.w(LOGTAG, "Sign in error", exception)
2017-01-25 13:08:23 +01:00
var errorReason: String? = null
2020-06-09 02:21:48 +02:00
when (exception) {
is AuthenticityTokenException -> {
Toast.makeText(this, R.string.message_toast_wrong_api_key, Toast.LENGTH_SHORT).show()
errorReason = "wrong_api_key"
}
is WrongUserPassException -> {
Toast.makeText(this, R.string.message_toast_wrong_username_password, Toast.LENGTH_SHORT).show()
errorReason = "wrong_username_password"
}
is SignInTask.WrongBasicCredentialException -> {
Toast.makeText(this, R.string.message_toast_wrong_username_password, Toast.LENGTH_SHORT).show()
errorReason = "wrong_username_password"
}
is SignInTask.WrongAPIURLFormatException -> {
Toast.makeText(this, R.string.message_toast_wrong_api_key, Toast.LENGTH_SHORT).show()
errorReason = "wrong_api_key"
}
is LoginVerificationException -> {
Toast.makeText(this, R.string.message_toast_login_verification_failed, Toast.LENGTH_SHORT).show()
errorReason = "login_verification_failed"
}
else -> {
Toast.makeText(this, exception.getErrorMessage(this), Toast.LENGTH_SHORT).show()
}
2017-01-25 13:08:23 +01:00
}
Analyzer.log(SignIn(false, credentialsType = apiConfig.credentialsType,
errorReason = errorReason, accountType = apiConfig.type))
}
2017-04-18 11:56:59 +02:00
2017-01-25 13:08:23 +01:00
internal fun onSignInStart() {
showSignInProgressDialog()
}
internal fun showSignInProgressDialog() {
executeAfterFragmentResumed {
if (isFinishing) return@executeAfterFragmentResumed
val fm = supportFragmentManager
val ft = fm.beginTransaction()
val fragment = ProgressDialogFragment()
fragment.isCancelable = false
fragment.show(ft, FRAGMENT_TAG_SIGN_IN_PROGRESS)
}
}
2017-08-28 11:50:04 +02:00
internal fun dismissSignInProgressDialog() {
dismissDialogFragment(FRAGMENT_TAG_SIGN_IN_PROGRESS)
}
2017-05-18 16:26:45 +02:00
private fun showLoginTypeChooser() {
2017-03-22 13:00:29 +01:00
executeAfterFragmentResumed {
2017-05-18 16:26:45 +02:00
val fm = it.supportFragmentManager
2017-06-13 06:05:36 +02:00
val df = SignInTypeChooserDialogFragment()
2017-05-18 16:26:45 +02:00
df.show(fm, "login_type_chooser")
}
}
private fun finishMastodonBrowserLogin(host: String, clientId: String, clientSecret: String, code: String) {
2017-05-19 17:08:23 +02:00
signInTask = MastodonLoginTask(this, host, clientId, clientSecret, code).apply { execute() }
}
2017-01-27 15:35:28 +01:00
private fun handleBrowserLoginResult(intent: Intent?) {
2016-07-07 09:39:32 +02:00
if (intent == null) return
2017-04-22 16:32:00 +02:00
val extras = intent.getBundleExtra(EXTRA_EXTRAS) ?: return
val requestToken = OAuthToken(extras.getString(EXTRA_REQUEST_TOKEN),
extras.getString(EXTRA_REQUEST_TOKEN_SECRET))
2017-05-13 12:59:13 +02:00
val verifier = intent.getStringExtra(EXTRA_OAUTH_VERIFIER)
2017-05-19 17:08:23 +02:00
signInTask = BrowserSignInTask(this, apiConfig, requestToken, verifier).apply { execute() }
2016-07-07 09:39:32 +02:00
}
private fun setDefaultAPI() {
if (!apiConfig.isDefault) return
val apiLastChange = preferences[apiLastChangeKey]
if (apiLastChange != apiChangeTimestamp) {
apiConfig = preferences[defaultAPIConfigKey]
2016-07-07 09:39:32 +02:00
apiChangeTimestamp = apiLastChange
}
2017-04-18 11:56:59 +02:00
updateSignInType()
setSignInButton()
}
private fun updateSignInType() {
// Mastodon have different case
if (apiConfig.type == AccountType.MASTODON) {
usernamePasswordContainer.visibility = View.VISIBLE
editPassword.visibility = View.GONE
2017-04-18 16:00:16 +02:00
editUsername.hint = getString(R.string.label_mastodon_host)
2017-04-18 11:56:59 +02:00
} else when (apiConfig.credentialsType) {
Credentials.Type.XAUTH, Credentials.Type.BASIC -> {
usernamePasswordContainer.visibility = View.VISIBLE
editPassword.visibility = View.VISIBLE
editUsername.hint = getString(R.string.label_username)
}
Credentials.Type.EMPTY -> {
usernamePasswordContainer.visibility = View.GONE
}
else -> {
usernamePasswordContainer.visibility = View.GONE
}
}
2016-07-07 09:39:32 +02:00
}
private fun setSignInButton() {
2017-04-18 11:56:59 +02:00
// Mastodon have different case
if (apiConfig.type == AccountType.MASTODON) {
passwordSignIn.visibility = View.GONE
signIn.isEnabled = true
} else when (apiConfig.credentialsType) {
2016-12-03 06:48:40 +01:00
Credentials.Type.XAUTH, Credentials.Type.BASIC -> {
2016-07-07 09:39:32 +02:00
passwordSignIn.visibility = View.GONE
2020-01-26 08:35:15 +01:00
signIn.isEnabled = !(editPassword.text.isNullOrEmpty() || editUsername.text.isNullOrEmpty())
2016-07-07 09:39:32 +02:00
}
2016-12-03 06:48:40 +01:00
Credentials.Type.OAUTH -> {
2016-07-07 09:39:32 +02:00
passwordSignIn.visibility = View.VISIBLE
signIn.isEnabled = true
}
else -> {
passwordSignIn.visibility = View.GONE
signIn.isEnabled = true
}
}
signUp.visibility = if (apiConfig.signUpUrlOrDefault != null) {
2017-01-19 12:43:20 +01:00
View.VISIBLE
} else {
View.GONE
}
passwordSignIn.visibility = if (apiConfig.type == null || apiConfig.type == AccountType.TWITTER) {
View.VISIBLE
} else {
View.GONE
}
2016-07-07 09:39:32 +02:00
}
2016-12-17 05:10:24 +01:00
private fun finishSignIn() {
if (accountAuthenticatorResponse != null) {
accountAuthenticatorResult = Bundle {
this[AccountManager.KEY_BOOLEAN_RESULT] = true
}
} else {
val intent = Intent(this, HomeActivity::class.java)
2017-01-07 17:13:11 +01:00
//TODO refresh timelines
2016-12-17 05:10:24 +01:00
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
startActivity(intent)
2016-12-03 06:48:40 +01:00
}
2016-12-17 05:10:24 +01:00
finish()
2016-07-07 09:39:32 +02:00
}
2017-06-13 06:05:36 +02:00
class SignInTypeChooserDialogFragment : BaseDialogFragment(),
2017-04-18 11:56:59 +02:00
LoaderManager.LoaderCallbacks<List<CustomAPIConfig>> {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
2017-04-18 11:56:59 +02:00
builder.setView(R.layout.dialog_expandable_list)
val dialog = builder.create()
dialog.onShow {
it.applyTheme()
val listView = it.expandableList
val adapter = LoginTypeAdapter(requireContext())
2017-04-18 11:56:59 +02:00
listView.setAdapter(adapter)
listView.setOnGroupClickListener { _, _, groupPosition, _ ->
val type = adapter.getGroup(groupPosition)
if (type.hasChildren) return@setOnGroupClickListener false
val activity = activity as? SignInActivity
val config = type.configs.single()
activity?.let {
it.apiConfig = config
it.updateSignInType()
it.setSignInButton()
}
dismiss()
return@setOnGroupClickListener true
}
listView.setOnChildClickListener { _, _, groupPosition, childPosition, _ ->
val config = adapter.getChild(groupPosition, childPosition)
val activity = activity as? SignInActivity
activity?.let {
it.apiConfig = config
it.updateSignInType()
it.setSignInButton()
}
dismiss()
return@setOnChildClickListener true
}
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
loaderManager.initLoader(0, null, this)
2016-12-03 06:48:40 +01:00
}
2017-04-18 11:56:59 +02:00
return dialog
2016-12-03 06:48:40 +01:00
}
2017-04-18 11:56:59 +02:00
override fun onLoadFinished(loader: Loader<List<CustomAPIConfig>>, data: List<CustomAPIConfig>) {
val dialog = dialog ?: return
2017-06-19 06:11:28 +02:00
val listView: ExpandableListView = dialog.findViewById(R.id.expandableList)
val defaultConfig = preferences[defaultAPIConfigKey]
2017-05-21 04:11:37 +02:00
defaultConfig.name = getString(R.string.login_type_user_settings)
2017-05-03 15:42:05 +02:00
val allConfig = ArraySet(data)
allConfig.add(defaultConfig)
val configGroup = allConfig.groupBy { it.safeType }
2017-04-22 09:28:12 +02:00
val supportedAccountTypes = arrayOf(AccountType.TWITTER, AccountType.FANFOU,
AccountType.MASTODON, AccountType.STATUSNET)
val result = supportedAccountTypes.mapNotNullTo(ArrayList()) { type ->
if (type == AccountType.MASTODON) return@mapNotNullTo LoginType(type,
listOf(CustomAPIConfig.mastodon(requireContext())))
2017-05-03 15:42:05 +02:00
return@mapNotNullTo configGroup[type]?.let { list ->
LoginType(type, list.sortedBy { !it.isDefault })
}
}
(listView.expandableListAdapter as LoginTypeAdapter).data = result
2016-07-07 09:39:32 +02:00
}
2017-04-18 11:56:59 +02:00
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<CustomAPIConfig>> {
return DefaultAPIConfigLoader(requireContext())
2016-07-07 09:39:32 +02:00
}
2017-04-18 11:56:59 +02:00
override fun onLoaderReset(loader: Loader<List<CustomAPIConfig>>) {
2016-07-07 09:39:32 +02:00
}
2017-04-18 11:56:59 +02:00
private data class LoginType(val type: String, val configs: List<CustomAPIConfig>) {
val hasChildren = configs.size > 1
2016-07-07 09:39:32 +02:00
}
2017-04-18 11:56:59 +02:00
private class LoginTypeAdapter(val context: Context) : BaseExpandableListAdapter() {
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
private val inflater = LayoutInflater.from(context)
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
var data: List<LoginType>? = null
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
override fun getGroupCount() = data?.count() ?: 0
override fun getGroup(groupPosition: Int) = data!![groupPosition]
override fun getChild(groupPosition: Int, childPosition: Int) =
getGroup(groupPosition).configs[childPosition]
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
override fun getChildrenCount(groupPosition: Int): Int {
val size = getGroup(groupPosition).configs.size
if (size > 1) return size
return 0
2016-12-03 06:48:40 +01:00
}
2016-12-08 16:45:07 +01:00
2017-04-18 11:56:59 +02:00
override fun isChildSelectable(groupPosition: Int, childPosition: Int) = true
override fun hasStableIds() = false
override fun getGroupId(groupPosition: Int) = groupPosition.toLong()
override fun getChildId(groupPosition: Int, childPosition: Int): Long =
groupPosition.toLong().shl(32) or childPosition.toLong()
2016-12-03 06:48:40 +01:00
2017-04-18 11:56:59 +02:00
override fun getGroupView(groupPosition: Int, isExpanded: Boolean, convertView: View?,
parent: ViewGroup): View {
val view = convertView ?: inflater.inflate(android.R.layout.simple_expandable_list_item_1, parent, false)
2017-06-19 06:11:28 +02:00
val text1 = view.findViewById<TextView>(android.R.id.text1)
val group = getGroup(groupPosition)
2017-05-03 15:42:05 +02:00
text1.text = APIEditorDialogFragment.getTypeTitle(context, group.type)
2017-04-18 11:56:59 +02:00
return view
}
override fun getChildView(groupPosition: Int, childPosition: Int, isLastChild: Boolean,
convertView: View?, parent: ViewGroup): View {
val view = convertView ?: inflater.inflate(android.R.layout.simple_list_item_1, parent, false)
val config = getChild(groupPosition, childPosition)
2017-06-19 06:11:28 +02:00
val text1 = view.findViewById<TextView>(android.R.id.text1)
2017-04-18 11:56:59 +02:00
text1.text = config.name
return view
}
2016-07-07 09:39:32 +02:00
}
}
2017-06-19 15:45:41 +02:00
internal class InputLoginVerificationDialogFragment : BaseDialogFragment() {
2016-07-07 09:39:32 +02:00
2017-08-28 11:50:04 +02:00
var deferred: Deferred<String?, Exception>? = null
2016-07-12 03:22:33 +02:00
var challengeType: String? = null
2016-07-07 09:39:32 +02:00
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
2016-07-07 09:39:32 +02:00
builder.setTitle(R.string.login_verification)
builder.setView(R.layout.dialog_login_verification_code)
2017-06-19 15:45:41 +02:00
builder.positive(android.R.string.ok, this::performVerification)
builder.negative(android.R.string.cancel, this::cancelVerification)
2016-07-07 09:39:32 +02:00
val dialog = builder.create()
2017-06-19 15:45:41 +02:00
dialog.onShow(this::onDialogShow)
2016-07-07 09:39:32 +02:00
return dialog
}
2020-01-26 08:35:15 +01:00
override fun onCancel(dialog: DialogInterface) {
2017-08-28 11:50:04 +02:00
deferred?.reject(CancelException())
2017-06-19 15:45:41 +02:00
}
private fun performVerification(dialog: Dialog) {
2017-08-28 11:50:04 +02:00
deferred?.resolve(dialog.editVerificationCode.string)
2017-06-19 15:45:41 +02:00
}
private fun cancelVerification(dialog: Dialog) {
2017-08-28 11:50:04 +02:00
deferred?.reject(CancelException())
2016-07-07 09:39:32 +02:00
}
2017-06-19 15:45:41 +02:00
private fun onDialogShow(dialog: Dialog) {
(dialog as? AlertDialog)?.applyTheme()
val verificationHint = dialog.verificationHint
val editVerification = dialog.editVerificationCode
2016-07-07 09:39:32 +02:00
if (verificationHint == null || editVerification == null) return
2016-07-12 03:22:33 +02:00
when {
"Push".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_verification_push_hint)
editVerification.visibility = View.GONE
}
"RetypePhoneNumber".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_challenge_retype_phone_hint)
editVerification.inputType = InputType.TYPE_CLASS_PHONE
editVerification.visibility = View.VISIBLE
}
"RetypeEmail".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_challenge_retype_email_hint)
editVerification.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
editVerification.visibility = View.VISIBLE
}
"Sms".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_verification_pin_hint)
editVerification.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
editVerification.visibility = View.VISIBLE
}
else -> {
verificationHint.text = getString(R.string.unsupported_login_verification_type_name,
challengeType)
editVerification.visibility = View.VISIBLE
}
2016-07-07 09:39:32 +02:00
}
}
}
class PasswordSignInDialogFragment : BaseDialogFragment() {
2017-04-18 11:56:59 +02:00
2016-07-07 09:39:32 +02:00
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
2016-07-07 09:39:32 +02:00
builder.setView(R.layout.dialog_password_sign_in)
2017-06-19 15:45:41 +02:00
builder.positive(R.string.action_sign_in, this::onPositiveButton)
2016-07-07 09:39:32 +02:00
builder.setNegativeButton(android.R.string.cancel, null)
val alertDialog = builder.create()
2017-06-19 15:45:41 +02:00
alertDialog.onShow { dialog ->
dialog.applyTheme()
val editUsername = dialog.editUsername
val editPassword = dialog.editPassword
2016-07-07 09:39:32 +02:00
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
2017-06-19 15:45:41 +02:00
val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE) ?: return
2017-04-18 11:56:59 +02:00
button.isEnabled = editUsername.length() > 0 && editPassword.length() > 0
2016-07-07 09:39:32 +02:00
}
override fun afterTextChanged(s: Editable) {
}
}
2017-04-18 11:56:59 +02:00
editUsername.addTextChangedListener(textWatcher)
editPassword.addTextChangedListener(textWatcher)
2016-07-07 09:39:32 +02:00
}
return alertDialog
}
2017-06-19 15:45:41 +02:00
private fun onPositiveButton(dialog: Dialog) {
val activity = activity as SignInActivity
val username = dialog.editUsername.string.orEmpty()
val password = dialog.editPassword.string.orEmpty()
activity.setDefaultAPI()
activity.performUserPassLogin(username, password)
}
2016-07-07 09:39:32 +02:00
}
internal class BrowserSignInTask(activity: SignInActivity, private val apiConfig: CustomAPIConfig,
2017-04-18 15:19:07 +02:00
private val requestToken: OAuthToken, private val oauthVerifier: String?) :
AbstractSignInTask(activity) {
2017-04-18 11:56:59 +02:00
@Throws(Exception::class)
override fun performLogin(): SignInResponse {
val context = activityRef.get() ?: throw InterruptedException()
2017-04-18 11:56:59 +02:00
val versionSuffix = if (apiConfig.isNoVersionSuffix) null else "1.1"
val apiUrlFormat = apiConfig.apiUrlFormat ?: throw MicroBlogException("No API URL format")
var auth = apiConfig.getOAuthAuthorization() ?:
throw MicroBlogException("Invalid OAuth credential")
var endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat,
apiConfig.isSameOAuthUrl)
val oauth = newMicroBlogInstance(context, endpoint = endpoint, auth = auth,
accountType = apiConfig.type, cls = TwitterOAuth::class.java)
val accessToken: OAuthToken
2020-06-08 23:19:10 +02:00
accessToken = if (oauthVerifier != null) {
oauth.getAccessToken(requestToken, oauthVerifier)
2017-04-18 11:56:59 +02:00
} else {
2020-06-08 23:19:10 +02:00
oauth.getAccessToken(requestToken)
2017-04-18 11:56:59 +02:00
}
auth = apiConfig.getOAuthAuthorization(accessToken) ?:
throw MicroBlogException("Invalid OAuth credential")
endpoint = MicroBlogAPIFactory.getOAuthEndpoint(apiUrlFormat, "api", versionSuffix,
apiConfig.isSameOAuthUrl)
val twitter = newMicroBlogInstance(context, endpoint = endpoint, auth = auth,
accountType = apiConfig.type, cls = MicroBlog::class.java)
val apiUser = twitter.verifyCredentials()
var color = analyseUserProfileColor(apiUser)
2020-06-08 23:01:17 +02:00
val (type, extras) = detectAccountType(twitter, apiUser, apiConfig.type)
2017-05-03 15:42:05 +02:00
val accountKey = apiUser.key
2017-04-20 19:23:11 +02:00
val user = apiUser.toParcelable(accountKey, type, profileImageSize = profileImageSize)
2017-04-18 11:56:59 +02:00
val am = AccountManager.get(context)
val account = AccountUtils.findByAccountKey(am, accountKey)
if (account != null) {
color = account.getColor(am)
}
val credentials = OAuthCredentials()
credentials.api_url_format = apiUrlFormat
credentials.no_version_suffix = apiConfig.isNoVersionSuffix
credentials.same_oauth_signing_url = apiConfig.isSameOAuthUrl
credentials.consumer_key = auth.consumerKey
credentials.consumer_secret = auth.consumerSecret
credentials.access_token = accessToken.oauthToken
credentials.access_token_secret = accessToken.oauthTokenSecret
return SignInResponse(account != null, Credentials.Type.OAUTH, credentials, user, color,
type, extras)
2017-04-18 11:56:59 +02:00
}
}
internal class MastodonLoginTask(context: SignInActivity, val host: String, val clientId: String,
val clientSecret: String, val code: String) : AbstractSignInTask(context) {
@Throws(Exception::class)
override fun performLogin(): SignInResponse {
val context = activityRef.get() ?: throw InterruptedException()
val oauth2 = newMicroBlogInstance(context, Endpoint("https://$host/"),
EmptyAuthorization(), AccountType.MASTODON, MastodonOAuth2::class.java)
val token = oauth2.getToken(clientId, clientSecret, code, MASTODON_CALLBACK_URL)
val endpoint = Endpoint("https://$host/api/")
val auth = OAuth2Authorization(token.accessToken)
val mastodon = newMicroBlogInstance(context, endpoint = endpoint, auth = auth,
accountType = AccountType.MASTODON, cls = Mastodon::class.java)
val apiAccount = mastodon.verifyCredentials()
var color = 0
val accountKey = UserKey(apiAccount.id, host)
val user = apiAccount.toParcelable(accountKey)
val am = AccountManager.get(context)
val account = AccountUtils.findByAccountKey(am, accountKey)
if (account != null) {
color = account.getColor(am)
}
val credentials = OAuth2Credentials()
credentials.api_url_format = endpoint.url
credentials.no_version_suffix = true
credentials.access_token = token.accessToken
return SignInResponse(account != null, Credentials.Type.OAUTH2, credentials, user,
color, AccountType.MASTODON, getMastodonAccountExtras(mastodon))
}
}
2016-07-07 09:39:32 +02:00
2016-12-03 06:48:40 +01:00
internal class SignInTask(
activity: SignInActivity,
private val username: String,
private val password: String,
2017-01-07 17:13:11 +01:00
private val apiConfig: CustomAPIConfig
2016-12-03 06:48:40 +01:00
) : AbstractSignInTask(activity) {
2017-01-07 17:13:11 +01:00
private val verificationCallback = InputLoginVerificationCallback()
private val userAgent = UserAgentUtils.getDefaultUserAgentString(activity)
private val apiUrlFormat = apiConfig.apiUrlFormat ?: DEFAULT_TWITTER_API_URL_FORMAT
2016-07-07 09:39:32 +02:00
2016-12-03 06:48:40 +01:00
@Throws(Exception::class)
override fun performLogin(): SignInResponse {
2017-01-07 17:13:11 +01:00
when (apiConfig.credentialsType) {
2016-12-03 06:48:40 +01:00
Credentials.Type.OAUTH -> return authOAuth()
Credentials.Type.XAUTH -> return authxAuth()
Credentials.Type.BASIC -> return authBasic()
Credentials.Type.EMPTY -> return authTwipOMode()
2016-07-07 09:39:32 +02:00
}
2016-12-03 06:48:40 +01:00
return authOAuth()
2016-07-07 09:39:32 +02:00
}
2020-06-08 23:01:17 +02:00
@Throws(AuthenticationException::class, MicroBlogException::class)
2016-07-07 09:39:32 +02:00
private fun authOAuth(): SignInResponse {
2016-12-03 06:48:40 +01:00
val activity = activityRef.get() ?: throw InterruptedException()
2016-07-07 09:39:32 +02:00
val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat,
2017-01-07 17:13:11 +01:00
apiConfig.isSameOAuthUrl)
2017-04-18 11:56:59 +02:00
val auth = apiConfig.getOAuthAuthorization() ?:
throw MicroBlogException("Invalid OAuth credential")
2016-12-08 16:45:07 +01:00
val oauth = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = TwitterOAuth::class.java)
2016-07-07 09:39:32 +02:00
val authenticator = OAuthPasswordAuthenticator(oauth,
verificationCallback, userAgent)
val accessToken = authenticator.getOAuthAccessToken(username, password)
2017-04-12 14:58:08 +02:00
val userId = accessToken.userId
return getOAuthSignInResponse(activity, accessToken, Credentials.Type.OAUTH)
2016-07-07 09:39:32 +02:00
}
@Throws(MicroBlogException::class)
private fun authxAuth(): SignInResponse {
2016-12-03 06:48:40 +01:00
val activity = activityRef.get() ?: throw InterruptedException()
2016-07-07 09:39:32 +02:00
var endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiUrlFormat,
2017-01-07 17:13:11 +01:00
apiConfig.isSameOAuthUrl)
2017-04-18 11:56:59 +02:00
var auth = apiConfig.getOAuthAuthorization() ?:
throw MicroBlogException("Invalid OAuth credential")
2016-12-08 16:45:07 +01:00
val oauth = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = TwitterOAuth::class.java)
2016-07-07 09:39:32 +02:00
val accessToken = oauth.getAccessToken(username, password)
2017-04-12 14:58:08 +02:00
val userId = accessToken.userId ?: run {
2016-07-07 09:39:32 +02:00
// Trying to fix up userId if accessToken doesn't contain one.
2017-04-18 11:56:59 +02:00
auth = apiConfig.getOAuthAuthorization(accessToken) ?:
throw MicroBlogException("Invalid OAuth credential")
endpoint = MicroBlogAPIFactory.getOAuthRestEndpoint(apiUrlFormat,
apiConfig.isSameOAuthUrl, apiConfig.isNoVersionSuffix)
2016-12-08 16:45:07 +01:00
val microBlog = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = MicroBlog::class.java)
2017-04-12 14:58:08 +02:00
return@run microBlog.verifyCredentials().id
2016-07-07 09:39:32 +02:00
}
return getOAuthSignInResponse(activity, accessToken, Credentials.Type.XAUTH)
2016-07-07 09:39:32 +02:00
}
2020-06-08 23:01:17 +02:00
@Throws(MicroBlogException::class, AuthenticationException::class)
2016-07-07 09:39:32 +02:00
private fun authBasic(): SignInResponse {
2016-12-03 06:48:40 +01:00
val activity = activityRef.get() ?: throw InterruptedException()
2017-01-07 17:13:11 +01:00
val versionSuffix = if (apiConfig.isNoVersionSuffix) null else "1.1"
2016-07-07 09:39:32 +02:00
val endpoint = Endpoint(MicroBlogAPIFactory.getApiUrl(apiUrlFormat, "api",
versionSuffix))
val auth = BasicAuthorization(username, password)
2016-12-08 16:45:07 +01:00
val twitter = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = MicroBlog::class.java)
2016-12-03 06:48:40 +01:00
val apiUser: User
2016-07-07 09:39:32 +02:00
try {
2016-12-03 06:48:40 +01:00
apiUser = twitter.verifyCredentials()
2016-07-07 09:39:32 +02:00
} catch (e: MicroBlogException) {
if (e.statusCode == 401) {
throw WrongBasicCredentialException()
} else if (e.statusCode == 404) {
throw WrongAPIURLFormatException()
}
throw e
}
2016-12-03 06:48:40 +01:00
var color = analyseUserProfileColor(apiUser)
2020-06-08 23:01:17 +02:00
val (type, extras) = detectAccountType(twitter, apiUser, apiConfig.type)
2017-05-03 15:42:05 +02:00
val accountKey = apiUser.key
2017-04-20 19:23:11 +02:00
val user = apiUser.toParcelable(accountKey, type, profileImageSize = profileImageSize)
2016-12-15 06:11:32 +01:00
val am = AccountManager.get(activity)
val account = AccountUtils.findByAccountKey(am, accountKey)
2016-07-07 09:39:32 +02:00
if (account != null) {
2016-12-15 06:11:32 +01:00
color = account.getColor(am)
2016-07-07 09:39:32 +02:00
}
2016-12-03 06:48:40 +01:00
val credentials = BasicCredentials()
credentials.api_url_format = apiUrlFormat
2017-01-07 17:13:11 +01:00
credentials.no_version_suffix = apiConfig.isNoVersionSuffix
2016-12-03 06:48:40 +01:00
credentials.username = username
credentials.password = password
return SignInResponse(account != null, Credentials.Type.BASIC, credentials, user,
color, type, extras)
2016-07-07 09:39:32 +02:00
}
@Throws(MicroBlogException::class)
private fun authTwipOMode(): SignInResponse {
2016-12-03 06:48:40 +01:00
val activity = activityRef.get() ?: throw InterruptedException()
2017-01-07 17:13:11 +01:00
val versionSuffix = if (apiConfig.isNoVersionSuffix) null else "1.1"
2016-07-07 09:39:32 +02:00
val endpoint = Endpoint(MicroBlogAPIFactory.getApiUrl(apiUrlFormat, "api",
versionSuffix))
val auth = EmptyAuthorization()
2016-12-08 16:45:07 +01:00
val twitter = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = MicroBlog::class.java)
2016-12-03 06:48:40 +01:00
val apiUser = twitter.verifyCredentials()
var color = analyseUserProfileColor(apiUser)
2020-06-08 23:01:17 +02:00
val (type, extras) = detectAccountType(twitter, apiUser, apiConfig.type)
2017-05-03 15:42:05 +02:00
val accountKey = apiUser.key
2017-04-20 19:23:11 +02:00
val user = apiUser.toParcelable(accountKey, type, profileImageSize = profileImageSize)
2016-12-15 06:11:32 +01:00
val am = AccountManager.get(activity)
val account = AccountUtils.findByAccountKey(am, accountKey)
2016-07-07 09:39:32 +02:00
if (account != null) {
2016-12-15 06:11:32 +01:00
color = account.getColor(am)
2016-07-07 09:39:32 +02:00
}
2016-12-03 06:48:40 +01:00
val credentials = EmptyCredentials()
credentials.api_url_format = apiUrlFormat
2017-01-07 17:13:11 +01:00
credentials.no_version_suffix = apiConfig.isNoVersionSuffix
2016-12-03 06:48:40 +01:00
return SignInResponse(account != null, Credentials.Type.EMPTY, credentials, user, color,
type, extras)
2016-07-07 09:39:32 +02:00
}
@Throws(MicroBlogException::class)
2017-04-18 11:56:59 +02:00
private fun getOAuthSignInResponse(activity: SignInActivity, accessToken: OAuthToken,
@Credentials.Type authType: String): SignInResponse {
2017-04-18 11:56:59 +02:00
val auth = apiConfig.getOAuthAuthorization(accessToken) ?:
throw MicroBlogException("Invalid OAuth credential")
2016-07-07 09:39:32 +02:00
val endpoint = MicroBlogAPIFactory.getOAuthRestEndpoint(apiUrlFormat,
2017-01-07 17:13:11 +01:00
apiConfig.isSameOAuthUrl, apiConfig.isNoVersionSuffix)
2016-12-08 16:45:07 +01:00
val twitter = newMicroBlogInstance(activity, endpoint = endpoint, auth = auth,
2017-02-11 10:03:35 +01:00
accountType = apiConfig.type, cls = MicroBlog::class.java)
2016-12-03 06:48:40 +01:00
val apiUser = twitter.verifyCredentials()
var color = analyseUserProfileColor(apiUser)
2020-06-08 23:01:17 +02:00
val (type, extras) = detectAccountType(twitter, apiUser, apiConfig.type)
2017-05-03 15:42:05 +02:00
val accountKey = apiUser.key
2017-04-20 19:23:11 +02:00
val user = apiUser.toParcelable(accountKey, type, profileImageSize = profileImageSize)
2016-12-15 06:11:32 +01:00
val am = AccountManager.get(activity)
val account = AccountUtils.findByAccountKey(am, accountKey)
2016-07-07 09:39:32 +02:00
if (account != null) {
2016-12-15 06:11:32 +01:00
color = account.getColor(am)
2016-07-07 09:39:32 +02:00
}
2016-12-03 06:48:40 +01:00
val credentials = OAuthCredentials()
credentials.api_url_format = apiUrlFormat
2017-01-07 17:13:11 +01:00
credentials.no_version_suffix = apiConfig.isNoVersionSuffix
2016-12-03 06:48:40 +01:00
2017-01-07 17:13:11 +01:00
credentials.same_oauth_signing_url = apiConfig.isSameOAuthUrl
2016-12-03 06:48:40 +01:00
2017-04-18 11:56:59 +02:00
credentials.consumer_key = auth.consumerKey
credentials.consumer_secret = auth.consumerSecret
2016-12-03 06:48:40 +01:00
credentials.access_token = accessToken.oauthToken
credentials.access_token_secret = accessToken.oauthTokenSecret
return SignInResponse(account != null, authType, credentials, user, color, type, extras)
2016-07-07 09:39:32 +02:00
}
2020-06-08 23:01:17 +02:00
internal class WrongBasicCredentialException : AuthenticationException()
2016-07-07 09:39:32 +02:00
2020-06-08 23:01:17 +02:00
internal class WrongAPIURLFormatException : AuthenticationException()
2016-07-07 09:39:32 +02:00
2020-06-08 23:01:17 +02:00
internal inner class InputLoginVerificationCallback : LoginVerificationCallback {
2016-07-07 09:39:32 +02:00
2016-07-14 14:00:27 +02:00
override fun getLoginVerification(challengeType: String): String? {
2016-07-07 09:39:32 +02:00
// Dismiss current progress dialog
publishProgress(Runnable {
2017-08-28 11:50:04 +02:00
activityRef.get()?.dismissSignInProgressDialog()
2016-07-07 09:39:32 +02:00
})
2017-08-28 11:50:04 +02:00
val deferred = deferred<String?, Exception>()
2016-07-07 09:39:32 +02:00
// Show verification input dialog and wait for user input
publishProgress(Runnable {
val activity = activityRef.get() ?: return@Runnable
2017-08-28 11:50:04 +02:00
activity.executeAfterFragmentResumed {
val sia = it as SignInActivity
2016-07-07 09:39:32 +02:00
val df = InputLoginVerificationDialogFragment()
df.isCancelable = false
2017-08-28 11:50:04 +02:00
df.deferred = deferred
2016-07-12 03:22:33 +02:00
df.challengeType = challengeType
2016-12-15 06:49:18 +01:00
df.show(sia.supportFragmentManager, "login_challenge_$challengeType")
2016-07-07 09:39:32 +02:00
}
})
2017-08-28 11:50:04 +02:00
return try {
deferred.promise.get()
} catch (e: CancelException) {
throw MicroBlogException(e)
} finally {
// Show progress dialog
publishProgress(Runnable {
activityRef.get()?.showSignInProgressDialog()
})
2016-07-07 09:39:32 +02:00
}
}
}
}
internal abstract class AbstractSignInTask(activity: SignInActivity) : AsyncTask<Any, Runnable, SingleResponse<SignInResponse>>() {
protected val activityRef = WeakReference(activity)
protected val profileImageSize: String = activity.getString(R.string.profile_image_size)
2019-10-24 17:52:11 +02:00
final override fun doInBackground(vararg args: Any?): SingleResponse<SignInResponse> {
2020-06-08 23:19:10 +02:00
return try {
SingleResponse.getInstance(performLogin())
} catch (e: Exception) {
2020-06-08 23:19:10 +02:00
SingleResponse.getInstance(e)
}
}
abstract fun performLogin(): SignInResponse
override fun onPostExecute(result: SingleResponse<SignInResponse>) {
val activity = activityRef.get()
activity?.dismissDialogFragment(FRAGMENT_TAG_SIGN_IN_PROGRESS)
if (result.hasData()) {
activity?.onSignInResult(result.data!!)
} else {
activity?.onSignInError(result.exception!!)
}
}
override fun onPreExecute() {
val activity = activityRef.get()
activity?.onSignInStart()
}
override fun onProgressUpdate(vararg values: Runnable) {
for (value in values) {
value.run()
}
}
@Throws(MicroBlogException::class)
internal fun analyseUserProfileColor(user: User?): Int {
if (user == null) throw MicroBlogException("Unable to get user info")
return ParcelableUserUtils.parseColor(user.profileLinkColor)
}
}
2017-04-18 15:19:07 +02:00
internal data class SignInResponse(
val alreadyLoggedIn: Boolean,
@Credentials.Type val credsType: String = Credentials.Type.EMPTY,
val credentials: Credentials,
val user: ParcelableUser,
val color: Int = 0,
val type: String,
val extras: AccountExtras?
2017-04-18 15:19:07 +02:00
) {
private fun writeAccountInfo(action: (k: String, v: String?) -> Unit) {
action(ACCOUNT_USER_DATA_KEY, user.key.toString())
action(ACCOUNT_USER_DATA_TYPE, type)
2017-04-18 15:19:07 +02:00
action(ACCOUNT_USER_DATA_CREDS_TYPE, credsType)
action(ACCOUNT_USER_DATA_ACTIVATED, true.toString())
action(ACCOUNT_USER_DATA_COLOR, toHexColor(color, format = HexColorFormat.RGB))
action(ACCOUNT_USER_DATA_USER, JsonSerializer.serialize(user))
action(ACCOUNT_USER_DATA_EXTRAS, extras?.let { JsonSerializer.serialize(it) })
2017-04-18 15:19:07 +02:00
}
private fun writeAuthToken(am: AccountManager, account: Account) {
val authToken = JsonSerializer.serialize(credentials)
am.setAuthToken(account, ACCOUNT_AUTH_TOKEN_TYPE, authToken)
}
fun updateAccount(am: AccountManager) {
val account = AccountUtils.findByAccountKey(am, user.key) ?: return
writeAccountInfo { k, v ->
am.setUserData(account, k, v)
}
writeAuthToken(am, account)
}
fun addAccount(am: AccountManager, randomizeAccountName: Boolean): Account {
var accountName: String
if (randomizeAccountName) {
val usedNames = ArraySet<String>()
AccountUtils.getAccounts(am).mapTo(usedNames, Account::name)
do {
accountName = UUID.randomUUID().toString()
} while (accountName in usedNames)
} else {
accountName = generateAccountName(user.screen_name, user.key.host)
}
val account = Account(accountName, ACCOUNT_TYPE)
val accountPosition = AccountUtils.getAccounts(am).size
// Don't add UserData in this method, see http://stackoverflow.com/a/29776224/859190
am.addAccountExplicitly(account, null, null)
writeAccountInfo { k, v ->
am.setUserData(account, k, v)
}
am.setUserData(account, ACCOUNT_USER_DATA_POSITION, accountPosition.toString())
writeAuthToken(am, account)
return account
}
}
2016-07-07 09:39:32 +02:00
companion object {
2017-04-18 15:19:07 +02:00
const val REQUEST_BROWSER_TWITTER_SIGN_IN = 101
const val REQUEST_BROWSER_MASTODON_SIGN_IN = 102
2016-07-07 09:39:32 +02:00
2020-06-08 23:11:06 +02:00
private const val FRAGMENT_TAG_SIGN_IN_PROGRESS = "sign_in_progress"
private const val EXTRA_API_LAST_CHANGE = "api_last_change"
2016-07-07 09:39:32 +02:00
2017-01-07 17:13:11 +01:00
@Throws(IOException::class)
internal fun detectAccountType(twitter: MicroBlog, user: User, type: String?): Pair<String, AccountExtras?> {
when (type) {
AccountType.STATUSNET -> {
return Pair(type, getStatusNetAccountExtras(twitter))
2017-01-07 17:13:11 +01:00
}
AccountType.TWITTER -> {
return Pair(type, getTwitterAccountExtras(twitter))
2017-01-07 17:13:11 +01:00
}
AccountType.FANFOU -> {
return Pair(AccountType.FANFOU, null)
}
else -> {
2017-05-03 15:42:05 +02:00
if (user.isFanfouUser) {
2017-01-07 17:13:11 +01:00
return Pair(AccountType.FANFOU, null)
}
2016-07-07 09:39:32 +02:00
}
}
return Pair(AccountType.TWITTER, null)
2016-07-07 09:39:32 +02:00
}
2017-01-25 13:08:23 +01:00
private fun getStatusNetAccountExtras(twitter: MicroBlog): StatusNetAccountExtras {
2017-01-25 13:08:23 +01:00
// Get StatusNet specific resource
val config = twitter.statusNetConfig
2017-05-24 07:34:33 +02:00
return StatusNetAccountExtras().apply {
textLimit = config.site?.textLimit ?: -1
uploadLimit = config.attachments?.fileQuota ?: -1L
2017-01-25 13:08:23 +01:00
}
}
private fun getTwitterAccountExtras(twitter: MicroBlog): TwitterAccountExtras {
val extras = TwitterAccountExtras()
try {
// Get Twitter official only resource
val paging = Paging()
paging.count(1)
twitter.getActivitiesAboutMe(paging)
extras.setIsOfficialCredentials(true)
} catch (e: MicroBlogException) {
// Ignore
}
return extras
}
private fun getMastodonAccountExtras(mastodon: Mastodon): MastodonAccountExtras {
return MastodonAccountExtras()
2017-01-25 13:08:23 +01:00
}
2016-07-07 09:39:32 +02:00
2017-04-18 11:56:59 +02:00
private val CustomAPIConfig.signUpUrlOrDefault: String?
get() = signUpUrl ?: when (type) {
AccountType.TWITTER -> "https://twitter.com/signup"
AccountType.FANFOU -> "https://fanfou.com/register"
2017-04-18 11:56:59 +02:00
else -> null
}
2017-08-28 11:50:04 +02:00
}
2016-07-07 09:39:32 +02:00
}