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

343 lines
14 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.ActivityNotFoundException
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.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
2020-03-05 20:36:23 +01:00
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
2020-03-05 20:36:23 +01:00
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.di.PixelfedAPIHolder
import com.h.pixeldroid.objects.*
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.Utils
import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain
import io.reactivex.Single
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.functions.Function3
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_login.*
import okhttp3.HttpUrl
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
/**
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]
+----> [promptOAuth]
+---->____________________________
|[PixelfedAPI.instance] |
|[PixelfedAPI.obtainToken] |
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ +----> [PixelfedAPI.verifyCredentials]
*/
class LoginActivity : AppCompatActivity() {
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
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private lateinit var pixelfedAPI: PixelfedAPI
private var inputVisibility: Int = View.GONE
2020-03-05 19:02:22 +01:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
2020-03-05 20:36:23 +01:00
(application as Pixeldroid).getAppComponent().inject(this)
loadingAnimation(true)
appName = getString(R.string.app_name)
oauthScheme = getString(R.string.auth_scheme)
preferences = getSharedPreferences("$PACKAGE_ID.pref", Context.MODE_PRIVATE)
if (Utils.hasInternet(applicationContext)) {
connect_instance_button.setOnClickListener {
registerAppToServer(normalizeDomain(editText.text.toString()))
}
whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
inputVisibility = View.VISIBLE
} else {
login_activity_connection_required.visibility = View.VISIBLE
login_activity_connection_required_button.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 i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse("https://pixelfed.org/join")
startActivity(i)
}
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) {
try{
HttpUrl.Builder().host(normalizedDomain.replace("https://", "")).scheme("https").build()
} catch (e: IllegalArgumentException) {
return failedRegistration(getString(R.string.invalid_domain))
}
hideKeyboard()
loadingAnimation(true)
pixelfedAPI = apiHolder.setDomain(normalizedDomain)
Single.zip(
pixelfedAPI.registerApplication(
appName,"$oauthScheme://$PACKAGE_ID", SCOPE
),
pixelfedAPI.wellKnownNodeInfo(),
BiFunction<Application, NodeInfoJRD, Pair<Application, NodeInfoJRD>> { application, nodeInfoJRD ->
// we get here when both results have come in:
Pair(application, nodeInfoJRD)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Application, NodeInfoJRD>> {
override fun onSuccess(pair: Pair<Application, NodeInfoJRD>) {
val (credentials, nodeInfoJRD) = pair
val clientId = credentials.client_id ?: return failedRegistration()
preferences.edit()
.putString("domain", normalizedDomain)
.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 failedRegistration(getString(R.string.instance_error))
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
}
override fun onError(e: Throwable) {
//Error in any of the two requests will get to this
Log.e("registerAppToServer", e.message.toString())
failedRegistration()
}
override fun onSubscribe(d: Disposable) {}
})
}
private fun nodeInfoSchema(
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
) {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl).enqueue(object : Callback<NodeInfo> {
override fun onResponse(call: Call<NodeInfo>, response: Response<NodeInfo>) {
if (response.body() == null || !response.isSuccessful) {
return failedRegistration(getString(R.string.instance_error))
}
val nodeInfo = response.body() as NodeInfo
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
val builder = AlertDialog.Builder(this@LoginActivity)
builder.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()
}
}
// Create the AlertDialog
builder.show()
return
} else {
promptOAuth(normalizedDomain, clientId)
}
}
override fun onFailure(call: Call<NodeInfo>, t: Throwable) {
failedRegistration(getString(R.string.instance_error))
}
})
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"
val intent = CustomTabsIntent.Builder().build()
try {
2020-03-07 18:13:26 +01:00
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
2020-03-06 18:24:20 +01:00
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if (browserIntent.resolveActivity(packageManager) != null) {
startActivity(browserIntent)
} else {
2020-03-07 18:31:58 +01:00
return failedRegistration(getString(R.string.browser_launch_failed))
2020-03-06 18:24:20 +01:00
}
}
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 = apiHolder.setDomain(domain)
2020-03-07 18:13:26 +01:00
//TODO check why we can't do onErrorReturn { null } which would make more sense ¯\_(ツ)_/¯
//Also, maybe find a nicer way to do this, this feels hacky (although it can work fine)
val nullInstance = Instance(null, null, null, null, null, null, null, null)
val nullToken = Token(null, null, null, null)
Single.zip(
pixelfedAPI.instance().onErrorReturn { nullInstance },
pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).onErrorReturn { nullToken },
BiFunction<Instance, Token, Pair<Instance, Token>> { instance, token ->
// we get here when all results have come in:
Pair(instance, token)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Instance, Token>> {
override fun onSuccess(triple: Pair<Instance, Token>) {
val (instance, token) = triple
if(token == nullToken || token.access_token == null){
return failedRegistration(getString(R.string.token_error))
} else if(instance == nullInstance || instance.uri == null){
return failedRegistration(getString(R.string.instance_error))
}
2020-03-07 18:13:26 +01:00
DBUtils.storeInstance(db, instance)
storeUser(token.access_token, instance.uri)
wipeSharedSettings()
}
2020-03-07 18:13:26 +01:00
override fun onError(e: Throwable) {
Log.e("saveUserAndInstance", e.message.toString())
failedRegistration(getString(R.string.token_error))
}
override fun onSubscribe(d: Disposable) {}
})
2020-03-07 18:13:26 +01:00
}
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
loadingAnimation(false)
2020-03-07 18:13:26 +01:00
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) {
login_activity_instance_input_layout.visibility = View.GONE
progressLayout.visibility = View.VISIBLE
}
else {
login_activity_instance_input_layout.visibility = inputVisibility
progressLayout.visibility = View.GONE
}
}
private fun storeUser(accessToken: String, instance: String) {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.body() != null && response.isSuccessful) {
db.userDao().deActivateActiveUsers()
val user = response.body() as Account
DBUtils.addUser(
db,
user,
instance,
activeUser = true,
accessToken = accessToken
)
apiHolder.setDomainToCurrentUser(db)
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
}
})
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
}