PixelDroid-App-Android/app/src/main/java/com/h/pixeldroid/LoginActivity.kt

325 lines
12 KiB
Kotlin

package com.h.pixeldroid
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.databinding.ActivityLoginBinding
import com.h.pixeldroid.utils.*
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.*
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.utils.db.storeInstance
import kotlinx.coroutines.*
import retrofit2.HttpException
import java.io.IOException
import java.lang.IllegalArgumentException
/**
Overview of the flow of the login process: (boxes are requests done in parallel,
since they do not depend on each other)
_________________________________
|[PixelfedAPI.registerApplication]|
|[PixelfedAPI.wellKnownNodeInfo] |
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅
+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed)
+----> [promptOAuth]
+----> [PixelfedAPI.obtainToken]
+----> [PixelfedAPI.verifyCredentials]
*/
class LoginActivity : BaseActivity() {
companion object {
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
private const val SCOPE = "read write follow"
}
private lateinit var oauthScheme: String
private lateinit var appName: String
private lateinit var preferences: SharedPreferences
private lateinit var pixelfedAPI: PixelfedAPI
private var inputVisibility: Int = View.GONE
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
loadingAnimation(true)
appName = getString(R.string.app_name)
oauthScheme = getString(R.string.auth_scheme)
preferences = getSharedPreferences("$PACKAGE_ID.pref", Context.MODE_PRIVATE)
if (hasInternet(applicationContext)) {
binding.connectInstanceButton.setOnClickListener {
registerAppToServer(normalizeDomain(binding.editText.text.toString()))
}
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
inputVisibility = View.VISIBLE
} else {
binding.loginActivityConnectionRequired.visibility = View.VISIBLE
binding.loginActivityConnectionRequiredButton.setOnClickListener {
finish()
startActivity(intent)
}
}
loadingAnimation(false)
}
override fun onStart() {
super.onStart()
val url: Uri? = intent.data
//Check if the activity was started after the authentication
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
loadingAnimation(true)
val code = url.getQueryParameter("code")
authenticate(code)
}
override fun onStop() {
super.onStop()
loadingAnimation(false)
}
private fun whatsAnInstance() {
val builder = AlertDialog.Builder(this)
builder.apply {
setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
setPositiveButton(android.R.string.ok) { _, _ -> }
}
// Create the AlertDialog
builder.show()
}
private fun hideKeyboard() {
val view = currentFocus
if (view != null) {
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
view.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
}
private fun registerAppToServer(normalizedDomain: String) {
if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain))
hideKeyboard()
loadingAnimation(true)
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
lifecycleScope.launch {
try {
val credentialsDeferred: Deferred<Application?> = async {
try {
pixelfedAPI.registerApplication(
appName, "$oauthScheme://$PACKAGE_ID", SCOPE
)
} catch (exception: IOException) {
return@async null
} catch (exception: HttpException) {
return@async null
}
}
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
val credentials = credentialsDeferred.await()
val clientId = credentials?.client_id ?: return@launch failedRegistration()
preferences.edit()
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
}?.href ?: return@launch failedRegistration(getString(R.string.instance_error))
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
} catch (exception: IOException) {
return@launch failedRegistration()
} catch (exception: HttpException) {
return@launch failedRegistration()
}
}
}
private suspend fun nodeInfoSchema(
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
) = coroutineScope {
val nodeInfo: NodeInfo = try {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
} catch (exception: IOException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
} catch (exception: HttpException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
}
val domain: String = try {
if (nodeInfo.hasInstanceEndpointInfo()) {
storeInstance(db, nodeInfo)
nodeInfo.metadata?.config?.site?.url
} else {
val instance: Instance = try {
pixelfedAPI.instance()
} catch (exception: IOException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
} catch (exception: HttpException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
}
storeInstance(db, nodeInfo = null, instance = instance)
instance.uri
}
} catch (e: IllegalArgumentException){ null }
?: return@coroutineScope failedRegistration(getString(R.string.instance_error))
preferences.edit().putString("domain", normalizeDomain(domain)).apply()
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
AlertDialog.Builder(this@LoginActivity).apply {
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
}
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}.show()
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
AlertDialog.Builder(this@LoginActivity).apply {
setMessage(R.string.api_not_enabled_dialog)
setNegativeButton(android.R.string.ok) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}.show()
} else {
promptOAuth(normalizedDomain, clientId)
}
}
private fun promptOAuth(normalizedDomain: String, client_id: String) {
val url = "$normalizedDomain/oauth/authorize?" +
"client_id" + "=" + client_id + "&" +
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
"response_type=code" + "&" +
"scope=$SCOPE"
if (!openUrl(url)) return failedRegistration(getString(R.string.browser_launch_failed))
}
private fun authenticate(code: String?) {
// Get previous values from preferences
val domain = preferences.getString("domain", "") as String
val clientId = preferences.getString("clientID", "") as String
val clientSecret = preferences.getString("clientSecret", "") as String
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
return failedRegistration(getString(R.string.auth_failed))
}
//Successful authorization
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
lifecycleScope.launch {
try {
val token = pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
)
if (token.access_token == null) {
return@launch failedRegistration(getString(R.string.token_error))
}
storeUser(
token.access_token,
token.refresh_token,
clientId,
clientSecret,
domain
)
wipeSharedSettings()
} catch (exception: IOException) {
return@launch failedRegistration(getString(R.string.token_error))
} catch (exception: HttpException) {
return@launch failedRegistration(getString(R.string.token_error))
}
}
}
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
loadingAnimation(false)
binding.editText.error = message
wipeSharedSettings()
}
private fun wipeSharedSettings(){
preferences.edit().remove("domain").remove("clientId").remove("clientSecret")
.apply()
}
private fun loadingAnimation(on: Boolean){
if(on) {
binding.loginActivityInstanceInputLayout.visibility = View.GONE
binding.progressLayout.visibility = View.VISIBLE
}
else {
binding.loginActivityInstanceInputLayout.visibility = inputVisibility
binding.progressLayout.visibility = View.GONE
}
}
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
try {
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
db.userDao().deActivateActiveUsers()
addUser(
db,
user,
instance,
activeUser = true,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
apiHolder.setToCurrentUser()
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
} catch (exception: IOException) {
return failedRegistration(getString(R.string.verify_credentials))
} catch (exception: HttpException) {
return failedRegistration(getString(R.string.verify_credentials))
}
}
}