app/src/main/java/com/geekorum/ttrss/accounts/LoginViewModel.kt
changeset 0 14443efede32
child 33 67a8d23e6fc3
equal deleted inserted replaced
-1:000000000000 0:14443efede32
       
     1 /**
       
     2  * Geekttrss is a RSS feed reader application on the Android Platform.
       
     3  *
       
     4  * Copyright (C) 2017-2018 by Frederic-Charles Barthelery.
       
     5  *
       
     6  * This file is part of Geekttrss.
       
     7  *
       
     8  * Geekttrss is free software: you can redistribute it and/or modify
       
     9  * it under the terms of the GNU General Public License as published by
       
    10  * the Free Software Foundation, either version 3 of the License, or
       
    11  * (at your option) any later version.
       
    12  *
       
    13  * Geekttrss is distributed in the hope that it will be useful,
       
    14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    16  * GNU General Public License for more details.
       
    17  *
       
    18  * You should have received a copy of the GNU General Public License
       
    19  * along with Geekttrss.  If not, see <http://www.gnu.org/licenses/>.
       
    20  */
       
    21 package com.geekorum.ttrss.accounts
       
    22 
       
    23 import android.view.inputmethod.EditorInfo
       
    24 import androidx.annotation.StringRes
       
    25 import androidx.annotation.VisibleForTesting
       
    26 import androidx.databinding.InverseMethod
       
    27 import androidx.lifecycle.MutableLiveData
       
    28 import androidx.lifecycle.Transformations
       
    29 import com.geekorum.geekdroid.app.lifecycle.CoroutinesViewModel
       
    30 import com.geekorum.geekdroid.app.lifecycle.Event
       
    31 import com.geekorum.ttrss.R
       
    32 import com.geekorum.ttrss.network.ApiCallException
       
    33 import com.geekorum.ttrss.network.impl.LoginRequestPayload
       
    34 import kotlinx.coroutines.launch
       
    35 import kotlinx.coroutines.withContext
       
    36 import okhttp3.HttpUrl
       
    37 import javax.inject.Inject
       
    38 import kotlin.coroutines.CoroutineContext
       
    39 
       
    40 /**
       
    41  * ViewModel for LoginActivity
       
    42  */
       
    43 internal class LoginViewModel @Inject constructor(
       
    44     private val accountManager: TinyrssAccountManager,
       
    45     private val networkComponentBuilder: AuthenticatorNetworkComponent.Builder
       
    46 ) : CoroutinesViewModel() {
       
    47 
       
    48     var username = ""
       
    49     var password = ""
       
    50     var httpUrl: HttpUrl? = null
       
    51     private lateinit var action: String
       
    52     private var account: Account? = null
       
    53 
       
    54     val loginInProgress = MutableLiveData<Boolean>()
       
    55     val loginFailedEvent = MutableLiveData<Event<LoginFailedError>>()
       
    56     val actionCompleteEvent = MutableLiveData<Event<ActionCompleteEvent>>()
       
    57     val fieldErrors = MutableLiveData<FieldErrorStatus>().apply {
       
    58         value = FieldErrorStatus()
       
    59     }
       
    60 
       
    61     val areFieldsCorrect = Transformations.map(fieldErrors) {
       
    62         val editionDone = (it.hasEditAllFields || (!canChangeUsernameOrUrl && it.hasEditPassword))
       
    63         editionDone && it.areFieldsCorrect
       
    64     }
       
    65 
       
    66     val canChangeUsernameOrUrl: Boolean
       
    67         get() = (action != LoginActivity.ACTION_CONFIRM_CREDENTIALS)
       
    68 
       
    69     fun initialize(action: String, account: Account? = null) {
       
    70         check(action in listOf(LoginActivity.ACTION_ADD_ACCOUNT, LoginActivity.ACTION_CONFIRM_CREDENTIALS)) {
       
    71             "unknown action"
       
    72         }
       
    73         this.action = action
       
    74         this.account = account
       
    75         username = account?.username ?: ""
       
    76         httpUrl = HttpUrl.parse(account?.url ?: "")
       
    77     }
       
    78 
       
    79     fun checkValidUrl(text: CharSequence) {
       
    80         val current = checkNotNull(fieldErrors.value)
       
    81         val invalidUrlMsgId = when {
       
    82             text.isEmpty() -> R.string.error_field_required
       
    83             httpUrl == null -> R.string.error_invalid_http_url
       
    84             else -> null
       
    85         }
       
    86         fieldErrors.value = current.copy(invalidUrlMsgId = invalidUrlMsgId, hasEditUrl = true)
       
    87     }
       
    88 
       
    89     fun checkNonEmptyPassword(text: CharSequence) {
       
    90         val current = checkNotNull(fieldErrors.value)
       
    91         val invalidPasswordMsgId = when {
       
    92             text.isEmpty() -> R.string.error_field_required
       
    93             else -> null
       
    94         }
       
    95         fieldErrors.value = current.copy(invalidPasswordMsgId = invalidPasswordMsgId, hasEditPassword = true)
       
    96     }
       
    97 
       
    98     fun checkNonEmptyUsername(text: CharSequence) {
       
    99         val current = checkNotNull(fieldErrors.value)
       
   100         val invalidNameMsgId = when {
       
   101             text.isEmpty() -> R.string.error_field_required
       
   102             else -> null
       
   103         }
       
   104         fieldErrors.value = current.copy(invalidNameMsgId = invalidNameMsgId, hasEditName = true)
       
   105     }
       
   106 
       
   107     private fun checkFieldsCorrect(): Boolean {
       
   108         val current = checkNotNull(fieldErrors.value)
       
   109         fieldErrors.value = current.copy(hasAttemptLogin = true)
       
   110         return httpUrl != null &&  fieldErrors.value!!.areFieldsCorrect
       
   111     }
       
   112 
       
   113     @JvmOverloads
       
   114     fun confirmLogin(id: Int = EditorInfo.IME_NULL): Boolean {
       
   115         val handleAction = (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL)
       
   116         if (handleAction && checkFieldsCorrect()) {
       
   117             launch {
       
   118                 doLogin()
       
   119             }
       
   120         }
       
   121         return handleAction
       
   122     }
       
   123 
       
   124     @VisibleForTesting
       
   125     internal suspend fun doLogin(context: CoroutineContext = coroutineContext) = withContext(context) {
       
   126         val urlModule = TinyRssUrlModule(httpUrl!!.toString())
       
   127         val networkComponent = networkComponentBuilder
       
   128             .tinyRssUrlModule(urlModule)
       
   129             .build()
       
   130         val tinyRssApi = networkComponent.getTinyRssApi()
       
   131         val loginPayload = LoginRequestPayload(username, password)
       
   132 
       
   133         try {
       
   134             loginInProgress.value = true
       
   135             val response = tinyRssApi.login(loginPayload).await()
       
   136 
       
   137             when {
       
   138                 response.isStatusOk -> onUserLoggedIn()
       
   139 
       
   140                 response.error == ApiCallException.ApiError.LOGIN_FAILED
       
   141                         || response.error == ApiCallException.ApiError.NOT_LOGGED_IN ->
       
   142                     loginFailedEvent.value = LoginFailedEvent(R.string.error_login_failed)
       
   143 
       
   144                 else -> loginFailedEvent.value = LoginFailedEvent(R.string.error_unknown)
       
   145             }
       
   146         } catch (e: Exception) {
       
   147             loginFailedEvent.value = LoginFailedEvent(R.string.error_unknown)
       
   148         } finally {
       
   149             loginInProgress.value = false
       
   150         }
       
   151     }
       
   152 
       
   153 
       
   154     private fun onUserLoggedIn() {
       
   155         when (action) {
       
   156             LoginActivity.ACTION_CONFIRM_CREDENTIALS -> onConfirmCredentialsSuccess()
       
   157             LoginActivity.ACTION_ADD_ACCOUNT -> onAddAccountSuccess()
       
   158         }
       
   159     }
       
   160 
       
   161     private fun onConfirmCredentialsSuccess() {
       
   162         accountManager.updatePassword(account!!, password)
       
   163         actionCompleteEvent.value = ActionCompleteSuccessEvent(account!!)
       
   164     }
       
   165 
       
   166     private fun onAddAccountSuccess() {
       
   167         val result = addAccount()
       
   168         actionCompleteEvent.value = if (result != null) {
       
   169             ActionCompleteSuccessEvent(result)
       
   170         } else {
       
   171             ActionCompleteFailedEvent()
       
   172         }
       
   173     }
       
   174 
       
   175     private fun addAccount(): Account? {
       
   176         val account = Account(username, httpUrl!!.toString())
       
   177         val success = accountManager.addAccount(account, password)
       
   178         if (success) {
       
   179             accountManager.initializeAccountSync(account)
       
   180             return account
       
   181         }
       
   182         return null
       
   183     }
       
   184 
       
   185     data class LoginFailedError(@StringRes val errorMsgId: Int)
       
   186     private fun LoginFailedEvent(errorMsgId: Int) = Event(LoginFailedError(errorMsgId))
       
   187 
       
   188     sealed class ActionCompleteEvent {
       
   189         class Success(val account: Account) : ActionCompleteEvent()
       
   190         class Failed : ActionCompleteEvent()
       
   191     }
       
   192 
       
   193     private fun ActionCompleteSuccessEvent(account: Account) = Event(ActionCompleteEvent.Success(account))
       
   194     private fun ActionCompleteFailedEvent() = Event(ActionCompleteEvent.Failed())
       
   195 
       
   196     data class FieldErrorStatus(
       
   197         val invalidUrlMsgId: Int? = null,
       
   198         val invalidNameMsgId: Int? = null,
       
   199         val invalidPasswordMsgId: Int? = null,
       
   200         val hasEditUrl: Boolean = false,
       
   201         val hasEditPassword: Boolean = false,
       
   202         val hasEditName: Boolean = false,
       
   203         val hasAttemptLogin: Boolean = false
       
   204     ) {
       
   205         val areFieldsCorrect= (invalidNameMsgId == null && invalidPasswordMsgId == null && invalidUrlMsgId == null)
       
   206 
       
   207         val hasEditAllFields = (hasEditUrl && hasEditName && hasEditPassword)
       
   208     }
       
   209 
       
   210     companion object {
       
   211 
       
   212         @JvmStatic
       
   213         @InverseMethod("convertHttpUrlToString")
       
   214         fun convertStringToHttpUrl(url: String): HttpUrl? {
       
   215             return HttpUrl.parse(url)
       
   216         }
       
   217 
       
   218         @JvmStatic
       
   219         fun convertHttpUrlToString(url: HttpUrl?): String {
       
   220             // this method is only called when binding the model to the view
       
   221             // this doesn't happen when the user modify the content of the text field
       
   222             // so basically it only happen with url == null :
       
   223             // - on first initialisation
       
   224             // - on view recreation ?
       
   225             return url?.toString() ?: "https://"
       
   226         }
       
   227 
       
   228     }
       
   229 }