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

325 lines
12 KiB
Kotlin
Raw Normal View History

2020-03-05 19:02:22 +01:00
package com.h.pixeldroid
import android.app.AlertDialog
import android.content.Context
2020-03-06 18:24:20 +01:00
import android.content.Intent
import android.content.SharedPreferences
2020-03-05 20:36:23 +01:00
import android.net.Uri
2020-03-05 19:02:22 +01:00
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
2020-12-29 19:34:48 +01:00
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.databinding.ActivityLoginBinding
2021-02-04 20:44:31 +01:00
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
2021-02-04 20:44:31 +01:00
import kotlinx.coroutines.*
2020-12-29 19:34:48 +01:00
import retrofit2.HttpException
import java.io.IOException
2021-02-04 20:44:31 +01:00
import java.lang.IllegalArgumentException
2020-12-08 18:33:16 +01:00
/**
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]
*/
2020-12-11 16:53:12 +01:00
class LoginActivity : BaseActivity() {
companion object {
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
private const val SCOPE = "read write follow"
}
Feature/post creation (#83) * added perm and features for cameraS, gps and external storage * added camera activity accessible from main activity * added button to redirect to camera activity * implementing callback flow to use camera * working camera * added texture view for camera display * added camera activity * implemented texture listener * camera not working, flow done, no feedback implemented * camera working * refactored code, still an activity * added private to internal function, better error function handling * deleted camera activity * added camera fragment * added camera fragment * refactored camera as fragment * necessary dependencies for fragment testing * initial camera fragment test * corrected access to activity form fragment * Added state changes and termination * added lines to test, to test coverage * Removed unsupported state STARTED state transition * Added basic tests to test code coverage * use layout for tests, to trigger permissions requirements * grant camera permission to app in camera test * replaced null handlers by proper function getter * changed layout, added takePictureButton * using expresso to get code coverage on camea * take picture flow not finished * dummy change to camera test to perform new build * added connection flow before test to reach main activity * can take a picture and put it to ImageView * replaced button text with images * smaller buttons * test camera fragment buttons * added orientation handler * changed icon to make travis happy * test new espresso config for travis * removed useless rule * deleted useless val * added layout ID's * moved swipes from Before to Tests, and thread sleep * stoped swiping, now tests from fragment directly * start post creation flow * use Uri when taking photo, can now go back from picture preview * adjusted test and flow idea * tests on displayed UI elements for the post creation fragment * refactor camera fragment into transition new post fragemnt * finished first phase: get a picture Uri * fixed lint error found by travis CI * added global timeout to test * test the new way of test * refactor new way of testing * added in-app camera view and linked everything to the final flow + started API to post * strugling on the upload media part * upload image on server implemented * post upload implemented * added API call to get max_toot_chars and correct def of a post description * fixed some tests * fix tests: clicking on tabs make the app crash because of the camera fragment * comment problematic chunk of code while samuel tries to fix it * switch minimumsdk to api 24 * Revert "switch minimumsdk to api 24" This reverts commit 24ce46dd82038b59732fd958e5e071ded39cd549. * deactivited live camera for API 23 * tests for post creation fragment UI elements * remove worthless UI testing and add gallery intent test * removed camera intent for now * some refactor * lint error and more refactor * more refactor on merge from master * refactor and test for PostCreationActivity * Revert "refactor and test for PostCreationActivity" This reverts commit a0c146bcc545cdc3792df4806e6b0c908bd18747. * Revert "Revert "refactor and test for PostCreationActivity"" This reverts commit 147a9ed80d5f9c9e3c38b5a977786bfb39eeb1b6. * permissions correction for test * updtated test * fix a test and refactor * relink correct fragment * save picture locally * test post button * requested changes * fixed required changes * Revert "fixed required changes" This reverts commit 405a9d4d1af05353e30028e60041cc1c97569c1b. * redo change request * added /media api response to mockserver Co-authored-by: Andrea Clement <samuel.dietz@epfl.ch>
2020-04-24 12:44:12 +02:00
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
2020-03-05 19:02:22 +01:00
private lateinit var binding: ActivityLoginBinding
2020-03-05 19:02:22 +01:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
2020-03-05 20:36:23 +01:00
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() {
2020-03-06 18:24:20 +01:00
super.onStart()
val url: Uri? = intent.data
2020-03-06 18:24:20 +01:00
//Check if the activity was started after the authentication
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
loadingAnimation(true)
2020-03-07 18:13:26 +01:00
val code = url.getQueryParameter("code")
authenticate(code)
2020-03-06 18:24:20 +01:00
}
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
)
}
2020-03-07 18:13:26 +01:00
}
2020-03-07 18:13:26 +01:00
private fun registerAppToServer(normalizedDomain: String) {
2021-02-17 22:55:31 +01:00
if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain))
hideKeyboard()
loadingAnimation(true)
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
2020-12-29 19:34:48 +01:00
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
}
}
2020-12-29 19:34:48 +01:00
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
val credentials = credentialsDeferred.await()
val clientId = credentials?.client_id ?: return@launch failedRegistration()
2020-12-29 19:34:48 +01:00
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()
}
}
}
2020-12-29 19:34:48 +01:00
private suspend fun nodeInfoSchema(
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
2021-02-04 20:44:31 +01:00
) = coroutineScope {
val nodeInfo: NodeInfo = try {
2020-12-29 19:34:48 +01:00
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
} catch (exception: IOException) {
2021-02-04 20:44:31 +01:00
return@coroutineScope failedRegistration(getString(R.string.instance_error))
2020-12-29 19:34:48 +01:00
} catch (exception: HttpException) {
2021-02-04 20:44:31 +01:00
return@coroutineScope failedRegistration(getString(R.string.instance_error))
}
val domain: String = try {
if (nodeInfo.hasInstanceEndpointInfo()) {
storeInstance(db, nodeInfo)
nodeInfo.metadata?.config?.site?.url
2021-02-04 20:44:31 +01:00
} 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
2021-02-04 20:44:31 +01:00
}
} catch (e: IllegalArgumentException){ null }
?: return@coroutineScope failedRegistration(getString(R.string.instance_error))
2020-12-29 19:34:48 +01:00
preferences.edit().putString("domain", normalizeDomain(domain)).apply()
2021-02-04 20:44:31 +01:00
2020-12-29 19:34:48 +01:00
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
AlertDialog.Builder(this@LoginActivity).apply {
2020-12-29 19:34:48 +01:00
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
}
2020-12-29 19:34:48 +01:00
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()
2020-12-29 19:34:48 +01:00
} else {
promptOAuth(normalizedDomain, clientId)
}
2020-03-06 18:24:20 +01:00
}
2020-03-05 20:36:23 +01:00
2020-03-07 18:13:26 +01:00
private fun promptOAuth(normalizedDomain: String, client_id: String) {
2020-03-06 18:24:20 +01:00
val url = "$normalizedDomain/oauth/authorize?" +
"client_id" + "=" + client_id + "&" +
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
"response_type=code" + "&" +
2020-03-06 18:24:20 +01:00
"scope=$SCOPE"
if (!openUrl(url)) return failedRegistration(getString(R.string.browser_launch_failed))
2020-03-05 19:02:22 +01:00
}
2020-03-07 18:13:26 +01:00
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
2020-03-07 18:13:26 +01:00
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
2020-03-07 18:31:58 +01:00
return failedRegistration(getString(R.string.auth_failed))
2020-03-07 18:13:26 +01:00
}
//Successful authorization
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
2020-03-07 18:13:26 +01:00
2020-12-29 19:34:48 +01:00
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,
2021-02-04 20:44:31 +01:00
domain
2020-12-29 19:34:48 +01:00
)
wipeSharedSettings()
} catch (exception: IOException) {
return@launch failedRegistration(getString(R.string.token_error))
} catch (exception: HttpException) {
return@launch failedRegistration(getString(R.string.token_error))
}
}
2020-03-07 18:13:26 +01:00
}
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()
2020-03-07 18:13:26 +01:00
}
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
}
}
2020-12-29 19:34:48 +01:00
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()
2020-12-29 19:34:48 +01:00
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))
}
Feature/post creation (#83) * added perm and features for cameraS, gps and external storage * added camera activity accessible from main activity * added button to redirect to camera activity * implementing callback flow to use camera * working camera * added texture view for camera display * added camera activity * implemented texture listener * camera not working, flow done, no feedback implemented * camera working * refactored code, still an activity * added private to internal function, better error function handling * deleted camera activity * added camera fragment * added camera fragment * refactored camera as fragment * necessary dependencies for fragment testing * initial camera fragment test * corrected access to activity form fragment * Added state changes and termination * added lines to test, to test coverage * Removed unsupported state STARTED state transition * Added basic tests to test code coverage * use layout for tests, to trigger permissions requirements * grant camera permission to app in camera test * replaced null handlers by proper function getter * changed layout, added takePictureButton * using expresso to get code coverage on camea * take picture flow not finished * dummy change to camera test to perform new build * added connection flow before test to reach main activity * can take a picture and put it to ImageView * replaced button text with images * smaller buttons * test camera fragment buttons * added orientation handler * changed icon to make travis happy * test new espresso config for travis * removed useless rule * deleted useless val * added layout ID's * moved swipes from Before to Tests, and thread sleep * stoped swiping, now tests from fragment directly * start post creation flow * use Uri when taking photo, can now go back from picture preview * adjusted test and flow idea * tests on displayed UI elements for the post creation fragment * refactor camera fragment into transition new post fragemnt * finished first phase: get a picture Uri * fixed lint error found by travis CI * added global timeout to test * test the new way of test * refactor new way of testing * added in-app camera view and linked everything to the final flow + started API to post * strugling on the upload media part * upload image on server implemented * post upload implemented * added API call to get max_toot_chars and correct def of a post description * fixed some tests * fix tests: clicking on tabs make the app crash because of the camera fragment * comment problematic chunk of code while samuel tries to fix it * switch minimumsdk to api 24 * Revert "switch minimumsdk to api 24" This reverts commit 24ce46dd82038b59732fd958e5e071ded39cd549. * deactivited live camera for API 23 * tests for post creation fragment UI elements * remove worthless UI testing and add gallery intent test * removed camera intent for now * some refactor * lint error and more refactor * more refactor on merge from master * refactor and test for PostCreationActivity * Revert "refactor and test for PostCreationActivity" This reverts commit a0c146bcc545cdc3792df4806e6b0c908bd18747. * Revert "Revert "refactor and test for PostCreationActivity"" This reverts commit 147a9ed80d5f9c9e3c38b5a977786bfb39eeb1b6. * permissions correction for test * updtated test * fix a test and refactor * relink correct fragment * save picture locally * test post button * requested changes * fixed required changes * Revert "fixed required changes" This reverts commit 405a9d4d1af05353e30028e60041cc1c97569c1b. * redo change request * added /media api response to mockserver Co-authored-by: Andrea Clement <samuel.dietz@epfl.ch>
2020-04-24 12:44:12 +02:00
}
2020-03-05 19:02:22 +01:00
}