[API/Login] Make user action handling more universal.

This commit is contained in:
Kuba Szczodrzyński 2022-10-15 21:24:30 +02:00
parent 7ded400a30
commit 93ccdbdeb7
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
23 changed files with 206 additions and 174 deletions

View File

@ -34,6 +34,7 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
@ -69,6 +70,7 @@ import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.login.LoginProgressFragment
import pl.szczodrzynski.edziennik.ui.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment
@ -83,6 +85,7 @@ import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.*
@ -853,7 +856,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
app.userActionManager.execute(this, event.profileId, event.type, event.params)
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
}
private fun fragmentToSyncName(currentFragment: Int): Int {
@ -911,12 +914,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
false
}
"userActionRequired" -> {
app.userActionManager.execute(
this,
extras.getInt("profileId"),
extras.getInt("type"),
extras.getBundle("params"),
val event = UserActionRequiredEvent(
profileId = extras.getInt("profileId"),
type = extras.getEnum<UserActionRequiredEvent.Type>("type") ?: return,
params = extras.getBundle("params") ?: return,
errorText = 0,
)
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
true
}
"createManualEvent" -> {

View File

@ -84,19 +84,21 @@ class ApiService : Service() {
runTask()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
app.userActionManager.sendToUser(event)
taskRunning?.cancel()
clearTask()
runTask()
}
override fun onError(apiError: ApiError) {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
if (app.userActionManager.requiresUserAction(apiError)) {
app.userActionManager.sendToUser(apiError)
}
else {
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
}
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
if (apiError.isCritical) {
taskRunning?.cancel()

View File

@ -59,6 +59,9 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT

View File

@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
const val ERROR_FILE_DOWNLOAD = 113
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
const val ERROR_CAPTCHA_NEEDED = 3000
const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001
const val ERROR_REQUIRES_USER_ACTION = 114
const val ERROR_API_PDO_ERROR = 5000
const val ERROR_API_INVALID_CLIENT = 5001
@ -204,7 +200,6 @@ const val ERROR_PODLASIE_API_NO_TOKEN = 630
const val ERROR_PODLASIE_API_OTHER = 631
const val ERROR_PODLASIE_API_DATA_MISSING = 632
const val ERROR_USOS_OAUTH_LOGIN_REQUEST = 701
const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702
const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703
const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -162,6 +163,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -10,6 +10,7 @@ import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -148,12 +149,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
val error = if (response.code() == 200) null else
json.getJsonArray("errors")?.getString(0)
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
),
errorText = R.string.notification_user_action_required_captcha_librus,
)
return
}
error?.let { code ->
when {
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
// this doesn't work anyway: `errors` is an object with `g-recaptcha-response` set
code.contains("robotem") -> ERROR_CAPTCHA_LIBRUS_PORTAL
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
}.let { errorCode ->
@ -163,12 +175,6 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
return
}
}
if (json.getBoolean("captchaRequired") == true) {
data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL)
.withResponse(response)
.withApiResponse(json))
return
}
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
}

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -142,6 +143,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieData
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin.PodlasieFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -142,6 +143,10 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}

View File

@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData
import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -108,6 +109,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}

View File

@ -8,6 +8,7 @@ import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -88,6 +89,10 @@ class Usos(
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}

View File

@ -4,9 +4,11 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.Bundle
import pl.szczodrzynski.edziennik.ext.fromQueryString
@ -53,13 +55,16 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) {
"interactivity" to "confirm_user",
"oauth_token" to (data.oauthTokenKey ?: ""),
)
val params = Bundle(
"authorizeUrl" to "$authUrl?${authParams.toQueryString()}",
"redirectUrl" to USOS_API_OAUTH_REDIRECT_URL,
"responseStoreKey" to "oauthLoginResponse",
"extras" to data.loginStore.data.toBundle(),
data.requireUserAction(
type = UserActionRequiredEvent.Type.OAUTH,
params = Bundle(
"authorizeUrl" to "$authUrl?${authParams.toQueryString()}",
"redirectUrl" to USOS_API_OAUTH_REDIRECT_URL,
"responseStoreKey" to "oauthLoginResponse",
"extras" to data.loginStore.data.toBundle(),
),
errorText = R.string.notification_user_action_required_oauth_usos,
)
data.error(ApiError(TAG, ERROR_USOS_OAUTH_LOGIN_REQUEST).withParams(params))
}
}
@ -93,6 +98,7 @@ class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) {
data.oauthTokenKey = accessData["oauth_token"]
data.oauthTokenSecret = accessData["oauth_token_secret"]
data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null
data.loginStore.removeLoginData("oauthLoginResponse")
if (!data.oauthTokenIsUser)
data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -179,6 +180,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -6,12 +6,14 @@ package pl.szczodrzynski.edziennik.data.api.events
import android.os.Bundle
data class UserActionRequiredEvent(val profileId: Int, val type: Int, val params: Bundle?) {
companion object {
const val LOGIN_DATA_MOBIDZIENNIK = 101
const val LOGIN_DATA_LIBRUS = 102
const val LOGIN_DATA_VULCAN = 104
const val CAPTCHA_LIBRUS = 202
const val OAUTH_USOS = 701
data class UserActionRequiredEvent(
val profileId: Int?,
val type: Type,
val params: Bundle,
val errorText: Int,
) {
enum class Type {
RECAPTCHA,
OAUTH,
}
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.interfaces
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.Feature
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
@ -14,4 +15,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
*/
interface EdziennikCallback : EndpointCallback {
fun onCompleted()
fun onRequiresUserAction(event: UserActionRequiredEvent)
}

View File

@ -1,5 +1,6 @@
package pl.szczodrzynski.edziennik.data.api.models
import android.os.Bundle
import android.util.LongSparseArray
import android.util.SparseArray
import androidx.core.util.set
@ -12,7 +13,8 @@ import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.ext.*
@ -37,7 +39,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
/**
* A callback passed to all [Feature]s and [LoginMethod]s
*/
lateinit var callback: EndpointCallback
lateinit var callback: EdziennikCallback
/**
* A list of [LoginMethod]s *already fulfilled* during this sync.
@ -374,6 +376,15 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
callback.onError(apiError)
}
fun requireUserAction(type: UserActionRequiredEvent.Type, params: Bundle, errorText: Int) {
callback.onRequiresUserAction(UserActionRequiredEvent(
profileId = profile?.id,
type = type,
params = params,
errorText = errorText,
))
}
fun progress(step: Float) {
callback.onProgress(step)
}

View File

@ -22,6 +22,15 @@ fun Bundle?.getFloat(key: String, defaultValue: Float): Float {
fun Bundle?.getString(key: String, defaultValue: String): String {
return this?.getString(key, defaultValue) ?: defaultValue
}
inline fun <reified E : Enum<E>> Bundle?.getEnum(key: String): E? {
return this?.getString(key)?.let {
try {
enumValueOf<E>(it)
} catch (e: Exception) {
null
}
}
}
fun Bundle?.getIntOrNull(key: String): Int? {
return this?.get(key) as? Int
@ -48,6 +57,7 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
is Bundle -> putBundle(property.first, property.second as Bundle)
is Parcelable -> putParcelable(property.first, property.second as Parcelable)
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
is Enum<*> -> putString(property.first, (property.second as Enum<*>).name)
}
}
}

View File

@ -13,14 +13,16 @@ import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
class LibrusCaptchaDialog(
class RecaptchaPromptDialog(
activity: AppCompatActivity,
private val siteKey: String,
private val referer: String,
private val onSuccess: (recaptchaCode: String) -> Unit,
private val onFailure: (() -> Unit)?,
private val onCancel: (() -> Unit)?,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
override val TAG = "LibrusCaptchaDialog"
override val TAG = "RecaptchaPromptDialog"
override fun getTitleRes(): Int? = null
override fun inflate(layoutInflater: LayoutInflater) =
@ -46,8 +48,8 @@ class LibrusCaptchaDialog(
b.progress.visibility = View.VISIBLE
RecaptchaDialog(
activity,
siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U",
referer = "https://portal.librus.pl/rodzina/login",
siteKey = siteKey,
referer = referer,
onSuccess = { recaptchaCode ->
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
@ -67,6 +69,6 @@ class LibrusCaptchaDialog(
override fun onDismiss() {
if (!success)
onFailure?.invoke()
onCancel?.invoke()
}
}

View File

@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding
import pl.szczodrzynski.edziennik.ext.dp
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog
import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog
import pl.szczodrzynski.edziennik.ui.home.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
@ -85,11 +85,6 @@ class HomeDebugCard(
app.startActivity(Chucker.getLaunchIntent(activity, 1));
}
b.librusCaptchaButton.onClick {
//app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java))
LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {}).show()
}
b.getLogs.onClick {
val logs = HyperLog.getDeviceLogsInFile(activity, true)
val intent = Intent(Intent.ACTION_SEND)

View File

@ -65,7 +65,6 @@ object LoginInfo {
errorCodes = mapOf(
ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED to R.string.login_error_account_not_activated,
ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN to R.string.login_error_incorrect_login_or_password,
ERROR_CAPTCHA_LIBRUS_PORTAL to R.string.error_3001_reason
)
),
/*Mode(

View File

@ -19,7 +19,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_NEEDED
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION
import pl.szczodrzynski.edziennik.data.api.LOGIN_NO_ARGUMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
@ -29,6 +29,7 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding
import pl.szczodrzynski.edziennik.ext.joinNotNullStrings
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
@ -137,13 +138,21 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
return
}
app.userActionManager.execute(activity, event.profileId, event.type, event.params, onSuccess = { params ->
args.putAll(params)
doFirstLogin(args)
}, onFailure = {
activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED))
nav.navigateUp()
})
val callback = UserActionManager.UserActionCallback(
onSuccess = { data ->
args.putAll(data)
doFirstLogin(args)
},
onFailure = {
activity.error(ApiError(TAG, ERROR_REQUIRES_USER_ACTION))
nav.navigateUp()
},
onCancel = {
nav.navigateUp()
},
)
app.userActionManager.execute(activity, event, callback)
}
override fun onStart() {

View File

@ -16,13 +16,10 @@ import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_LIBRUS_PORTAL
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_OAUTH_LOGIN_REQUEST
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ui.captcha.LibrusCaptchaDialog
import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog
import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity
import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -32,43 +29,31 @@ class UserActionManager(val app: App) {
private const val TAG = "UserActionManager"
}
fun requiresUserAction(apiError: ApiError) = when (apiError.errorCode) {
ERROR_CAPTCHA_LIBRUS_PORTAL -> true
ERROR_USOS_OAUTH_LOGIN_REQUEST -> true
else -> false
}
fun sendToUser(apiError: ApiError) {
val type = when (apiError.errorCode) {
ERROR_CAPTCHA_LIBRUS_PORTAL -> UserActionRequiredEvent.CAPTCHA_LIBRUS
ERROR_USOS_OAUTH_LOGIN_REQUEST -> UserActionRequiredEvent.OAUTH_USOS
else -> 0
}
fun sendToUser(event: UserActionRequiredEvent) {
if (EventBus.getDefault().hasSubscriberForEvent(UserActionRequiredEvent::class.java)) {
EventBus.getDefault().post(UserActionRequiredEvent(apiError.profileId ?: -1, type, apiError.params))
EventBus.getDefault().post(event)
return
}
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val text = app.getString(when (type) {
UserActionRequiredEvent.CAPTCHA_LIBRUS -> R.string.notification_user_action_required_captcha_librus
UserActionRequiredEvent.OAUTH_USOS -> R.string.notification_user_action_required_oauth_usos
else -> R.string.notification_user_action_required_text
}, apiError.profileId)
val text = app.getString(event.errorText, event.profileId)
val intent = Intent(
app,
MainActivity::class.java,
"action" to "userActionRequired",
"profileId" to (apiError.profileId ?: -1),
"type" to type,
"params" to apiError.params,
app,
MainActivity::class.java,
"action" to "userActionRequired",
"profileId" to event.profileId,
"type" to event.type,
"params" to event.params,
)
val pendingIntent = PendingIntent.getActivity(
app,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag(),
)
val pendingIntent = PendingIntent.getActivity(app, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag())
val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key)
val notification =
NotificationCompat.Builder(app, app.notificationChannelsManager.userAttention.key)
.setContentTitle(app.getString(R.string.notification_user_action_required_title))
.setContentText(text)
.setSmallIcon(R.drawable.ic_error_outline)
@ -84,70 +69,56 @@ class UserActionManager(val app: App) {
manager.notify(System.currentTimeMillis().toInt(), notification)
}
class UserActionCallback(
val onSuccess: ((data: Bundle) -> Unit)? = null,
val onFailure: (() -> Unit)? = null,
val onCancel: (() -> Unit)? = null,
)
fun execute(
activity: AppCompatActivity,
profileId: Int?,
type: Int,
params: Bundle? = null,
onSuccess: ((params: Bundle) -> Unit)? = null,
onFailure: (() -> Unit)? = null
activity: AppCompatActivity,
event: UserActionRequiredEvent,
callback: UserActionCallback,
) {
d(TAG, "Running user action ($type) with params: ${params?.toJsonObject()}")
val isSuccessful = when (type) {
UserActionRequiredEvent.CAPTCHA_LIBRUS -> executeLibrus(activity, profileId, params, onSuccess, onFailure)
UserActionRequiredEvent.OAUTH_USOS -> executeOauth(activity, profileId, params, onSuccess, onFailure)
else -> false
}
if (!isSuccessful) {
onFailure?.invoke()
d(TAG, "Running user action (${event.type}) with params: ${event.params}")
val isSuccessful = when (event.type) {
UserActionRequiredEvent.Type.RECAPTCHA -> executeRecaptcha(activity, event, callback)
UserActionRequiredEvent.Type.OAUTH -> executeOauth(activity, event, callback)
}
if (!isSuccessful)
callback.onFailure?.invoke()
}
private fun executeLibrus(
private fun executeRecaptcha(
activity: AppCompatActivity,
profileId: Int?,
params: Bundle?,
onSuccess: ((params: Bundle) -> Unit)?,
onFailure: (() -> Unit)?,
event: UserActionRequiredEvent,
callback: UserActionCallback,
): Boolean {
if (profileId == null)
return false
val extras = params?.getBundle("extras")
// show captcha dialog
// use passed onSuccess listener, else sync profile
LibrusCaptchaDialog(
val siteKey = event.params.getString("siteKey") ?: return false
val referer = event.params.getString("referer") ?: return false
RecaptchaPromptDialog(
activity = activity,
siteKey = siteKey,
referer = referer,
onSuccess = { code ->
val args = Bundle(
finishAction(activity, event, callback, Bundle(
"recaptchaCode" to code,
"recaptchaTime" to System.currentTimeMillis(),
)
if (extras != null)
args.putAll(extras)
if (onSuccess != null)
onSuccess(args)
else
EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity)
))
},
onFailure = onFailure,
onCancel = callback.onCancel,
).show()
return true
}
private fun executeOauth(
activity: AppCompatActivity,
profileId: Int?,
params: Bundle?,
onSuccess: ((params: Bundle) -> Unit)?,
onFailure: (() -> Unit)?,
event: UserActionRequiredEvent,
callback: UserActionCallback,
): Boolean {
if (profileId == null || params == null)
return false
val extras = params.getBundle("extras")
val storeKey = params.getString("responseStoreKey") ?: return false
params.getString("authorizeUrl") ?: return false
params.getString("redirectUrl") ?: return false
val storeKey = event.params.getString("responseStoreKey") ?: return false
event.params.getString("authorizeUrl") ?: return false
event.params.getString("redirectUrl") ?: return false
var listener: Any? = null
listener = object {
@ -155,26 +126,41 @@ class UserActionManager(val app: App) {
fun onOAuthLoginResult(result: OAuthLoginResult) {
EventBus.getDefault().unregister(listener)
when {
result.isError -> onFailure?.invoke()
result.isError -> callback.onFailure?.invoke()
result.responseUrl != null -> {
val args = Bundle(
finishAction(activity, event, callback, Bundle(
storeKey to result.responseUrl,
)
if (extras != null)
args.putAll(extras)
if (onSuccess != null)
onSuccess(args)
else
EdziennikTask.syncProfile(profileId, arguments = args.toJsonObject()).enqueue(activity)
))
}
else -> callback.onCancel?.invoke()
}
}
}
EventBus.getDefault().register(listener)
val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(params)
val intent = Intent(activity, OAuthLoginActivity::class.java).putExtras(event.params)
activity.startActivity(intent)
return true
}
private fun finishAction(
activity: AppCompatActivity,
event: UserActionRequiredEvent,
callback: UserActionCallback,
data: Bundle,
) {
val extras = event.params.getBundle("extras")
if (extras != null)
data.putAll(extras)
if (callback.onSuccess != null)
callback.onSuccess.invoke(data)
else if (event.profileId != null)
EdziennikTask.syncProfile(
profileId = event.profileId,
arguments = data.toJsonObject(),
).enqueue(activity)
else
callback.onFailure?.invoke()
}
}

View File

@ -25,13 +25,6 @@
android:layout_height="wrap_content"
android:text="Save Debug Logs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/librusCaptchaButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="LIBRUS® Captcha" />
<com.google.android.material.button.MaterialButton
android:id="@+id/refreshWidget"
style="@style/Widget.MaterialComponents.Button"

View File

@ -31,11 +31,6 @@
<string name="error_112" translatable="false">ERROR_NOT_IMPLEMENTED</string>
<string name="error_113" translatable="false">ERROR_FILE_DOWNLOAD</string>
<string name="error_115" translatable="false">ERROR_NO_STUDENTS_IN_ACCOUNT</string>
<string name="error_3000" translatable="false">ERROR_CAPTCHA_NEEDED</string>
<string name="error_3001" translatable="false">ERROR_CAPTCHA_LIBRUS_PORTAL</string>
<string name="error_5000" translatable="false">ERROR_API_PDO_ERROR</string>
<string name="error_5001" translatable="false">ERROR_API_INVALID_CLIENT</string>
<string name="error_5002" translatable="false">ERROR_API_INVALID_ARGUMENT</string>
@ -174,8 +169,6 @@
<string name="error_631" translatable="false">ERROR_PODLASIE_API_OTHER</string>
<string name="error_632" translatable="false">ERROR_PODLASIE_API_DATA_MISSING</string>
<string name="error_701" translatable="false">ERROR_USOS_OAUTH_LOGIN_REQUEST</string>
<string name="error_801" translatable="false">ERROR_TEMPLATE_WEB_OTHER</string>
<string name="error_900" translatable="false">EXCEPTION_API_TASK</string>
@ -223,11 +216,6 @@
<string name="error_112_reason">Nie zaimplementowano</string>
<string name="error_113_reason">Wystąpił błąd podczas pobierania pliku. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_115_reason">Brak uczniów przypisanych do konta</string>
<string name="error_3000_reason">Wymagane rozwiązanie zadania Captcha</string>
<string name="error_3001_reason">LIBRUS®: wymagane rozwiązanie zadania Captcha</string>
<string name="error_5000_reason">ERROR_API_PDO_ERROR</string>
<string name="error_5001_reason">Nieprawidłowy ID klienta API</string>
<string name="error_5002_reason">API: nieprawidłowy argument</string>
@ -366,8 +354,6 @@
<string name="error_631_reason">ERROR_PODLASIE_API_OTHER</string>
<string name="error_632_reason">Brak danych. Zgłoś błąd programiście.</string>
<string name="error_701_reason">Wymagane logowanie w przeglądarce</string>
<string name="error_801_reason">ERROR_TEMPLATE_WEB_OTHER</string>
<string name="error_900_reason">Błąd synchronizacji. Upewnij się, że masz połączenie z internetem, a następnie zgłoś błąd.</string>