From f8b00e320f441e7cce69af55fb5fa0dae1a89825 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 31 Aug 2021 06:32:39 +0300 Subject: [PATCH 1/2] Removed unused classes Using dagger for Retrofit2, OkHttp and Gson --- app/build.gradle.kts | 9 ++ .../main/kotlin/com/meloda/fast/UserConfig.kt | 5 +- .../com/meloda/fast/VKLongPollParser.kt | 2 +- .../main/kotlin/com/meloda/fast/api/Answer.kt | 6 - .../kotlin/com/meloda/fast/api/Requests.kt | 2 - .../kotlin/com/meloda/fast/api/Resource.kt | 25 --- .../main/kotlin/com/meloda/fast/api/VKApi.kt | 36 +++-- .../main/kotlin/com/meloda/fast/api/VKAuth.kt | 27 +++- .../kotlin/com/meloda/fast/api/VKException.kt | 15 +- .../main/kotlin/com/meloda/fast/api/VKRepo.kt | 17 -- .../main/kotlin/com/meloda/fast/api/VKUrls.kt | 7 - .../com/meloda/fast/api/{util => }/VKUtil.kt | 18 ++- .../fast/api/datasource/MessagesDataSource.kt | 4 - .../api/datasource/base/BaseDataSource.kt | 97 ------------ .../com/meloda/fast/api/model/ApiResponse.kt | 12 -- .../com/meloda/fast/api/model/VKMessage.kt | 2 +- .../fast/api/network/AuthInterceptor.kt | 16 ++ .../fast/api/{ => network}/ErrorCodes.kt | 11 +- .../fast/api/network/ResultCallFactory.kt | 125 +++++++++++++++ .../com/meloda/fast/api/network/VKModules.kt | 65 ++++++++ .../com/meloda/fast/api/network/VKUrls.kt | 17 ++ .../api/network/datasource/AuthDataSource.kt | 10 ++ .../meloda/fast/api/network/repo/AuthRepo.kt | 13 ++ .../api/network/repo/ConversationsRepo.kt | 18 +++ .../fast/api/network/request/AuthRequest.kt | 37 +++++ .../network/request/ConversationsRequests.kt | 1 + .../RequestMessagesGetConversations.kt | 2 +- .../fast/api/network/response/AuthResponse.kt | 13 ++ .../response/MessagesResponse.kt | 2 +- .../fast/api/service/MessagesService.kt | 13 -- .../com/meloda/fast/base/BaseVMFragment.kt | 4 +- .../com/meloda/fast/base/viewmodel/BaseVM.kt | 33 ---- .../fast/base/viewmodel/BaseViewModel.kt | 57 +++++++ .../fast/database/storage/ChatsStorage.kt | 2 +- .../fast/database/storage/GroupsStorage.kt | 2 +- .../fast/database/storage/UsersStorage.kt | 2 +- .../fast/screens/login/LoginFragment.kt | 83 +++++----- .../com/meloda/fast/screens/login/LoginVM.kt | 127 --------------- .../fast/screens/login/LoginViewModel.kt | 147 ++++++++++++++++++ .../meloda/fast/screens/main/MainFragment.kt | 4 +- .../com/meloda/fast/screens/main/MainVM.kt | 5 - .../meloda/fast/screens/main/MainViewModel.kt | 5 + .../screens/messages/ConversationsFragment.kt | 4 +- ...sationsVM.kt => ConversationsViewModel.kt} | 4 +- .../kotlin/com/meloda/fast/util/VKUtils.kt | 2 +- .../kotlin/com/meloda/fast/util/ViewUtils.kt | 1 - app/src/main/res/layout/dialog_captcha.xml | 12 +- app/src/main/res/layout/fragment_login.xml | 6 - 48 files changed, 670 insertions(+), 457 deletions(-) delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/Answer.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/Requests.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/Resource.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/VKRepo.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/VKUrls.kt rename app/src/main/kotlin/com/meloda/fast/api/{util => }/VKUtil.kt (94%) delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/datasource/MessagesDataSource.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/datasource/base/BaseDataSource.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/ApiResponse.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt rename app/src/main/kotlin/com/meloda/fast/api/{ => network}/ErrorCodes.kt (86%) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/VKModules.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/request/AuthRequest.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/request/ConversationsRequests.kt rename app/src/main/kotlin/com/meloda/fast/api/{model => network}/request/RequestMessagesGetConversations.kt (91%) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt rename app/src/main/kotlin/com/meloda/fast/api/{model => network}/response/MessagesResponse.kt (86%) delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/service/MessagesService.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseVM.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/LoginVM.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/MainVM.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt rename app/src/main/kotlin/com/meloda/fast/screens/messages/{ConversationsVM.kt => ConversationsViewModel.kt} (70%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fef7ae5..f5afd0a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,8 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + +val login: String = gradleLocalProperties(rootDir).getProperty("vklogin") +val password: String = gradleLocalProperties(rootDir).getProperty("vkpassword") + plugins { id("com.android.application") id("kotlin-android") @@ -26,6 +31,10 @@ android { } buildTypes { + getByName("debug") { + buildConfigField("String", "vkLogin", login) + buildConfigField("String", "vkPassword", password) + } getByName("release") { isMinifyEnabled = false diff --git a/app/src/main/kotlin/com/meloda/fast/UserConfig.kt b/app/src/main/kotlin/com/meloda/fast/UserConfig.kt index 2a566452..2e3e40d7 100644 --- a/app/src/main/kotlin/com/meloda/fast/UserConfig.kt +++ b/app/src/main/kotlin/com/meloda/fast/UserConfig.kt @@ -29,7 +29,6 @@ object UserConfig { userId = -1 } - fun isLoggedIn(): Boolean { - return userId > 0 && !TextUtils.isEmpty(accessToken) - } + fun isLoggedIn() = userId > 0 && accessToken.isNotBlank() + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/VKLongPollParser.kt b/app/src/main/kotlin/com/meloda/fast/VKLongPollParser.kt index 738efeac..f203d761 100644 --- a/app/src/main/kotlin/com/meloda/fast/VKLongPollParser.kt +++ b/app/src/main/kotlin/com/meloda/fast/VKLongPollParser.kt @@ -6,7 +6,7 @@ import com.meloda.fast.concurrent.EventInfo import com.meloda.fast.concurrent.TaskManager import com.meloda.fast.api.VKApiKeys import com.meloda.fast.api.model.VKMessage -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import org.json.JSONArray @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/kotlin/com/meloda/fast/api/Answer.kt b/app/src/main/kotlin/com/meloda/fast/api/Answer.kt deleted file mode 100644 index f6fbffb5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/Answer.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.api - -sealed class Answer { - data class Success(val data: T) : Answer() - data class Error(val errorString: String) : Answer() -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/Requests.kt b/app/src/main/kotlin/com/meloda/fast/api/Requests.kt deleted file mode 100644 index 2e7b6b8a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/Requests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api - diff --git a/app/src/main/kotlin/com/meloda/fast/api/Resource.kt b/app/src/main/kotlin/com/meloda/fast/api/Resource.kt deleted file mode 100644 index 7d00e0e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/Resource.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api - -data class Resource constructor( - val status: Status, - val responseData: T?, - val message: String? -) { - - enum class Status { - SUCCESS, - ERROR, - LOADING - } - - companion object { - fun success(responseData: T?): Resource = - Resource(Status.SUCCESS, responseData, null) - - fun error(message: String?, responseBody: T? = null): Resource = - Resource(Status.ERROR, responseBody, message) - - fun loading(responseData: T? = null): Resource = - Resource(Status.LOADING, responseData, null) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKApi.kt b/app/src/main/kotlin/com/meloda/fast/api/VKApi.kt index a91b9112..33d23cc1 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKApi.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKApi.kt @@ -7,6 +7,7 @@ import com.meloda.fast.BuildConfig import com.meloda.fast.api.method.MessageMethodSetter import com.meloda.fast.api.method.MethodSetter import com.meloda.fast.api.method.UserMethodSetter +import com.meloda.fast.api.network.ErrorCodes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import org.json.JSONArray @@ -55,13 +56,14 @@ object VKApi { try { checkError(json, url) } catch (ex: VKException) { - if (ex.code == ErrorCodes.TOO_MANY_REQUESTS) { - Timer().schedule(object : TimerTask() { - override fun run() { - execute(url, cls) - } - }, 1000) - } else throw ex + throw ex +// if (ex.code == ErrorCodes.TOO_MANY_REQUESTS) { +// Timer().schedule(object : TimerTask() { +// override fun run() { +// execute(url, cls) +// } +// }, 1000) +// } else throw ex } when (cls) { @@ -282,7 +284,7 @@ object VKApi { val code = error.optInt("error_code", -1) val message = error.optString("error_msg", "") - val e = VKException(url, message, code) +// val e = VKException(url, message, code) //TODO: add checking invalid session if (code == 5 && message.contains("invalid session")) { @@ -291,16 +293,16 @@ object VKApi { // }) } - if (code == ErrorCodes.CAPTCHA_NEEDED) { - e.captchaImg = error.optString("captcha_img") - e.captchaSid = error.optString("captcha_sid") - } +// if (code == ErrorCodes.CAPTCHA_NEEDED) { +// e.captchaImg = error.optString("captcha_img") +// e.captchaSid = error.optString("captcha_sid") +// } +// +// if (code == ErrorCodes.VALIDATION_REQUIRED) { +// e.redirectUri = error.optString("redirect_uri") +// } - if (code == ErrorCodes.VALIDATION_REQUIRED) { - e.redirectUri = error.optString("redirect_uri") - } - - throw e +// throw e } } diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKAuth.kt b/app/src/main/kotlin/com/meloda/fast/api/VKAuth.kt index d201ed3b..f67a5338 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKAuth.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKAuth.kt @@ -3,14 +3,17 @@ package com.meloda.fast.api import android.util.Log import com.meloda.fast.BuildConfig import com.meloda.fast.UserConfig -import com.meloda.fast.api.util.VKUtil import java.net.URLEncoder object VKAuth { private const val TAG = "VKM.VKAuth" - private const val settings = "notify," + + object GrantType { + const val PASSWORD = "password" + } + + const val scope = "notify," + "friends," + "photos," + "audio," + @@ -30,17 +33,25 @@ object VKAuth { fun getDirectAuthUrl( login: String, password: String, - captchaSid: String? = null, - captchaKey: String? = null - ) = "https://oauth.vk.com/token?grant_type=password&" + + twoFa: Boolean = false, + twoFaCode: String = "", + captcha: Pair? = null + ) = "https://oauth.vk.com/token?" + + "grant_type=password&" + "client_id=${VKConstants.VK_APP_ID}&" + - "scope=$settings&" + "client_secret=${VKConstants.VK_SECRET}&" + "username=$login&" + - "password=$password" + - (if (captchaSid == null || captchaKey == null) "" else "&captcha_sid=$captchaSid&captcha_key=$captchaKey") + + "password=$password&" + + "scope=$scope&" + + "2fa_supported=1&" + + "force_sms=${if (twoFa) "1" else "0"}" + + (if (twoFa) "code=$twoFaCode" else "") + + (if (captcha == null) "" else "&captcha_sid=${captcha.first}&captcha_key=${captcha.second}") + "&v=${URLEncoder.encode(VKApi.API_VERSION, "utf-8")}" + fun getSendSmsCodeUrl(sid: String) = "https://api.vk.com/method/auth.validatePhone?" + + "sid=$sid&" + + "&v=${URLEncoder.encode(VKApi.API_VERSION, "utf-8")}" fun getOAuthUrl(settings: String) = "https://oauth.vk.com/authorize?" + "client_id=${UserConfig.FAST_APP_ID}&" + diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt index 9c2a94a2..1044a675 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt @@ -1,15 +1,20 @@ package com.meloda.fast.api +import org.json.JSONObject import java.io.IOException -class VKException(var url: String = "", override var message: String = "", var code: Int) : - IOException(message) { - var captchaSid: String? = null - var captchaImg: String? = null +class VKException(var url: String = "", var description: String = "", var error: String) : + IOException(description) { + + var captcha: Pair? = null var redirectUri: String? = null + var validationSid: String? = null + + var json: JSONObject? = null + override fun toString(): String { - return "code: $code, message: $message" + return "url: $url;\n\nerror: $error; description: $description;" } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/VKRepo.kt deleted file mode 100644 index 97ebfc17..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/VKRepo.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.api - -import com.meloda.fast.api.model.response.GetConversationsResponse -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface VKRepo { - - @FormUrlEncoded - @POST(VKUrls.getConversations) - suspend fun getAllChats( - @Field("user_id") chatId: Int, - @Field("token") token: String - ): Answer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/VKUrls.kt deleted file mode 100644 index b00610f3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/VKUrls.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.api - -object VKUrls { - - const val getConversations = "messages.getConversations" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/util/VKUtil.kt b/app/src/main/kotlin/com/meloda/fast/api/VKUtil.kt similarity index 94% rename from app/src/main/kotlin/com/meloda/fast/api/util/VKUtil.kt rename to app/src/main/kotlin/com/meloda/fast/api/VKUtil.kt index 09200155..ca604ea2 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/util/VKUtil.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKUtil.kt @@ -1,7 +1,8 @@ -package com.meloda.fast.api.util +package com.meloda.fast.api import androidx.annotation.WorkerThread import com.meloda.fast.api.model.* +import com.meloda.fast.api.network.VKErrors import org.json.JSONArray import org.json.JSONObject import java.text.SimpleDateFormat @@ -12,6 +13,21 @@ object VKUtil { private const val TAG = "VKUtil" + fun isValidationRequired(throwable: Throwable): Boolean { + if (throwable !is VKException) return false + return throwable.error == VKErrors.NEED_VALIDATION + } + + fun isCaptchaRequired(throwable: Throwable): Boolean { + if (throwable !is VKException) return false + return throwable.error == VKErrors.NEED_CAPTCHA + } + + fun extractValidationSid(throwable: Throwable): String? { + if (throwable !is VKException) return null + return throwable.json?.optString("validation_sid") + } + fun extractPattern(string: String, pattern: String): String? { val p = Pattern.compile(pattern) val m = p.matcher(string) diff --git a/app/src/main/kotlin/com/meloda/fast/api/datasource/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/datasource/MessagesDataSource.kt deleted file mode 100644 index b67c4553..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/datasource/MessagesDataSource.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.meloda.fast.api.datasource - -class MessagesDataSource constructor() { -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/datasource/base/BaseDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/datasource/base/BaseDataSource.kt deleted file mode 100644 index 5147bda6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/datasource/base/BaseDataSource.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.meloda.fast.api.datasource.base - -import android.util.Log -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.google.gson.JsonSyntaxException -import com.meloda.fast.api.Resource -import com.meloda.fast.api.model.ApiResponse -import com.meloda.fast.api.ErrorCodes -import com.meloda.fast.api.VKException -import okhttp3.ResponseBody -import retrofit2.HttpException - -class BaseDataSource { - - private val TAG = BaseDataSource::class.simpleName - - //TODO: move to resources - private val DEFAULT_ERROR = "Internal server error" - - protected suspend fun getResult(apiCall: suspend () -> ApiResponse): Resource { - try { - val response = apiCall() - return if (response.isSuccessful) { - Resource.success(response.response) - } else { - Log.d(TAG, "Server response unsuccessful") - if (response.error != null) { - Log.w(TAG, "Unsuccessful response with code 2XX") - Resource.error(response.error.message, response.response) - } else { - Log.e(TAG, "Unsuccessful result without error!") - Resource.error(DEFAULT_ERROR) - } - } - } catch (e: HttpException) { - Log.e(TAG, "Error while executing request ${e.message}") - val errorBody = e.response()?.errorBody() ?: return Resource.error(DEFAULT_ERROR) - val errorResponse = parseErrorBody(errorBody) ?: return Resource.error(DEFAULT_ERROR) - - return Resource.error(errorResponse.message) - } catch (e: Exception) { - Log.e(TAG, "Error while executing request ${e.message}") - - return Resource.error(DEFAULT_ERROR) - } - } - - private fun parseErrorBody(responseBody: ResponseBody?): Exception? { - if (responseBody == null) return null - - val jsonResponse: JsonObject? - try { - jsonResponse = JsonParser.parseString(responseBody.string()) as? JsonObject - if (jsonResponse == null) { - Log.d(TAG, "Response body is empty while parsing error body.") - return null - } - } catch (e: JsonSyntaxException) { - Log.e(TAG, "Error while parsing json ${e.message}") - return null - } catch (e: java.lang.Exception) { - Log.e(TAG, "Unknown error ${e.message}") - return null - } - - if (jsonResponse.has("error")) { - val error = jsonResponse["error"].asJsonObject - - val message = error["error_msg"].asString - val code = error["error_code"].asInt - - val e = VKException("", message, code) - - //TODO: add checking invalid session - if (code == 5 && message.contains("invalid session")) { -// context?.startActivity(Intent(context, DropUserDataActivity::class.java).apply { -// addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) -// }) - } - - if (code == ErrorCodes.CAPTCHA_NEEDED) { - e.captchaImg = error["captcha_img"].asString - e.captchaSid = error["captcha_sid"].asString - } - - if (code == ErrorCodes.VALIDATION_REQUIRED) { - e.redirectUri = error["redirect_uri"].asString - } - - return e - } - - return null - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/ApiResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/model/ApiResponse.kt deleted file mode 100644 index 43ef6478..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/ApiResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.api.model - -data class ApiResponse constructor( - val isSuccessful: Boolean, - val error: Error?, - val response: T? -) - -data class Error constructor( - val code: Long, - val message: String -) \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VKMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VKMessage.kt index e02555d1..919be666 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VKMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VKMessage.kt @@ -1,7 +1,7 @@ package com.meloda.fast.api.model import android.util.ArrayMap -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import org.json.JSONObject open class VKMessage() : VKModel() { diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt new file mode 100644 index 00000000..a3f3274c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt @@ -0,0 +1,16 @@ +package com.meloda.fast.api.network + +import com.meloda.fast.api.VKApi +import okhttp3.Interceptor +import okhttp3.Response +import java.net.URLEncoder + +class AuthInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val builder = chain.request().url.newBuilder() + .addQueryParameter("v", URLEncoder.encode(VKApi.API_VERSION, "utf-8")) + return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/ErrorCodes.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt similarity index 86% rename from app/src/main/kotlin/com/meloda/fast/api/ErrorCodes.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt index 748594f0..12c13ecd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/ErrorCodes.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api +package com.meloda.fast.api.network object ErrorCodes { const val UNKNOWN_ERROR = 1 @@ -39,4 +39,13 @@ object ErrorCodes { const val INVALID_DOC_ID = 1150 const val INVALID_DOC_TITLE = 1152 const val ACCESS_TO_DOC_DENIED = 1153 +} + +object VKErrors { + const val UNKNOWN = "unknown_error" + + const val NEED_VALIDATION = "need_validation" + const val NEED_CAPTCHA = "need_captcha" + const val INVALID_REQUEST = "invalid_request" + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt new file mode 100644 index 00000000..ddc3309c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt @@ -0,0 +1,125 @@ +package com.meloda.fast.api.network + +import android.util.Log +import com.meloda.fast.api.VKException +import okhttp3.Request +import okio.IOException +import okio.Timeout +import org.json.JSONObject +import retrofit2.* +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ResultCallFactory : CallAdapter.Factory() { + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + val rawReturnType: Class<*> = getRawType(returnType) + if (rawReturnType == Call::class.java) { + if (returnType is ParameterizedType) { + val callInnerType: Type = getParameterUpperBound(0, returnType) + if (getRawType(callInnerType) == Answer::class.java) { + if (callInnerType is ParameterizedType) { + val resultInnerType = getParameterUpperBound(0, callInnerType) + return ResultCallAdapter(resultInnerType) + } + return ResultCallAdapter(Nothing::class.java) + } + } + } + return null + } +} + +internal abstract class CallDelegate(protected val proxy: Call) : Call { + + override fun execute(): Response = throw NotImplementedError() + + final override fun enqueue(callback: Callback) = enqueueImpl(callback) + + final override fun clone(): Call = cloneImpl() + + override fun cancel() = proxy.cancel() + + override fun request(): Request = proxy.request() + + override fun isExecuted() = proxy.isExecuted + + override fun isCanceled() = proxy.isCanceled + + abstract fun enqueueImpl(callback: Callback) + + abstract fun cloneImpl(): Call +} + +private class ResultCallAdapter(private val type: Type) : CallAdapter>> { + + override fun responseType() = type + + override fun adapt(call: Call): Call> = ResultCall(call) +} + +internal class ResultCall(proxy: Call) : CallDelegate>(proxy) { + + override fun enqueueImpl(callback: Callback>) { + proxy.enqueue(ResultCallback(this, callback)) + } + + override fun cloneImpl(): ResultCall { + return ResultCall(proxy.clone()) + } + + private class ResultCallback( + private val proxy: ResultCall, + private val callback: Callback> + ) : Callback { + + // TODO: 8/31/2021 parse VK errors + override fun onResponse(call: Call, response: Response) { + val result: Answer = if (response.isSuccessful) + Answer.Success(response.body() as T) + else Answer.Error(IOException(response.errorBody()?.string() ?: "")) + + if (result is Answer.Error) if (checkErrors(call, result)) return + + callback.onResponse(proxy, Response.success(result)) + } + + override fun onFailure(call: Call, error: Throwable) { + callback.onResponse( + proxy, + Response.success(Answer.Error(throwable = error)) + ) + } + + private fun checkErrors(call: Call, result: Answer.Error): Boolean { + val json = JSONObject(result.throwable.message ?: "{}") + + return if (json.has("error")) { + val error = json.optString("error", "") + val description = json.optString("error_description", "") + + val exception = VKException( + error = error, + description = description, + ).also { it.json = json } + + onFailure(call, exception) + true + } else false + } + } + + override fun timeout(): Timeout { + return proxy.timeout() + } +} + +sealed class Answer { + + data class Success(val data: T) : Answer() + data class Error(val throwable: Throwable) : Answer() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKModules.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VKModules.kt new file mode 100644 index 00000000..b7c7ba1d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VKModules.kt @@ -0,0 +1,65 @@ +package com.meloda.fast.api.network + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.meloda.fast.api.network.datasource.AuthDataSource +import com.meloda.fast.api.network.repo.AuthRepo +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +class VKModules { + + @Singleton + @Provides + fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient = OkHttpClient.Builder() + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .addInterceptor(authInterceptor) + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }).build() + + @Singleton + @Provides + fun provideGson(): Gson = GsonBuilder() + .setLenient() + .create() + + @Singleton + @Provides + fun provideRetrofit( + client: OkHttpClient, + gson: Gson + ): Retrofit = Retrofit.Builder() + .baseUrl("https://api.vk.com/") + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(ResultCallFactory()) + .client(client) + .build() + + @Provides + @Singleton + fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor() + + @Provides + fun provideAuthRepo(retrofit: Retrofit): AuthRepo = + retrofit.create(AuthRepo::class.java) + + @Provides + fun provideAuthDataSource(repo: AuthRepo): AuthDataSource = + AuthDataSource(repo) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt new file mode 100644 index 00000000..5075877b --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt @@ -0,0 +1,17 @@ +package com.meloda.fast.api.network + +object VKUrls { + + object Auth { + const val directAuth = "https://oauth.vk.com/token" + } + + object Conversations { + const val get = "messages.getConversations" + } + + + +} + + diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt new file mode 100644 index 00000000..4c9a06fb --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt @@ -0,0 +1,10 @@ +package com.meloda.fast.api.network.datasource + +import com.meloda.fast.api.network.repo.AuthRepo +import javax.inject.Inject + +class AuthDataSource @Inject constructor( + private val repo: AuthRepo +) : AuthRepo { + override suspend fun auth(param: Map) = repo.auth(param) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt new file mode 100644 index 00000000..f05377ef --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt @@ -0,0 +1,13 @@ +package com.meloda.fast.api.network.repo + +import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.response.ResponseAuthDirect +import com.meloda.fast.api.network.Answer +import retrofit2.http.* + +interface AuthRepo { + + @GET(VKUrls.Auth.directAuth) + suspend fun auth(@QueryMap param: Map): Answer + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt new file mode 100644 index 00000000..0cd3ba30 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt @@ -0,0 +1,18 @@ +package com.meloda.fast.api.network.repo + +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.response.GetConversationsResponse +import retrofit2.http.* + +interface ConversationsRepo { + + @FormUrlEncoded + @POST(VKUrls.Conversations.get) + suspend fun getAllChats( + @Field("user_id") chatId: Int, + @Field("token") token: String + ): Answer + + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/request/AuthRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/request/AuthRequest.kt new file mode 100644 index 00000000..8e53e40a --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/request/AuthRequest.kt @@ -0,0 +1,37 @@ +package com.meloda.fast.api.network.request + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RequestAuthDirect( + @SerializedName("grant_type") val grantType: String, + @SerializedName("client_id") val clientId: String, + @SerializedName("client_secret") val clientSecret: String, + @SerializedName("username") val username: String, + @SerializedName("password") val password: String, + @SerializedName("scope") val scope: String, + @SerializedName("2fa_supported") val twoFaSupported: Boolean = true, + @SerializedName("force_sms") val twoFaForceSms: Boolean = false, + @SerializedName("code") val twoFaCode: String? = null, + @SerializedName("captcha_sid") val captchaSid: String? = null, + @SerializedName("captcha_key") val captchaKey: String? = null, +) : Parcelable { + val map + get() = mutableMapOf( + "grant_type" to grantType, + "client_id" to clientId, + "client_secret" to clientSecret, + "username" to username, + "password" to password, + "scope" to scope, + "2fa_supported" to if (twoFaSupported) "1" else "0", + "force_sms" to if (twoFaForceSms) "1" else "0" + ) + .apply { + twoFaCode?.let { this["code"] = it } + captchaSid?.let { this["captcha_sid"] = it } + captchaKey?.let { this["captcha_key"] = it } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/request/ConversationsRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/request/ConversationsRequests.kt new file mode 100644 index 00000000..9eb8dcd5 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/request/ConversationsRequests.kt @@ -0,0 +1 @@ +package com.meloda.fast.api.network.request diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/RequestMessagesGetConversations.kt b/app/src/main/kotlin/com/meloda/fast/api/network/request/RequestMessagesGetConversations.kt similarity index 91% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/RequestMessagesGetConversations.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/request/RequestMessagesGetConversations.kt index 17f4d2ca..8c99e76b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/RequestMessagesGetConversations.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/request/RequestMessagesGetConversations.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.request import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt new file mode 100644 index 00000000..dded6222 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt @@ -0,0 +1,13 @@ +package com.meloda.fast.api.network.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ResponseAuthDirect( + @SerializedName("access_token") val accessToken: String? = null, + @SerializedName("user_id") val userId: Int? = null, + @SerializedName("trusted_hash") val twoFaHash: String? = null, + @SerializedName("validation_sid") val validationSid: String? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/response/MessagesResponse.kt similarity index 86% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/response/MessagesResponse.kt index 46d10bdf..e9542f23 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/response/MessagesResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.response import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/service/MessagesService.kt b/app/src/main/kotlin/com/meloda/fast/api/service/MessagesService.kt deleted file mode 100644 index 28b12721..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/service/MessagesService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.service - -import com.meloda.fast.api.model.ApiResponse -import com.meloda.fast.api.model.request.RequestMessagesGetConversations -import retrofit2.http.GET -import retrofit2.http.QueryMap - -interface MessagesService { - - @GET("messages.getConversations") - suspend fun getConversations(@QueryMap params: RequestMessagesGetConversations): ApiResponse> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseVMFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseVMFragment.kt index e7c5de1c..be8aac75 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseVMFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseVMFragment.kt @@ -4,12 +4,12 @@ import android.os.Bundle import android.view.View import androidx.annotation.LayoutRes import androidx.lifecycle.lifecycleScope -import com.meloda.fast.base.viewmodel.BaseVM +import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.VKEvent import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach -abstract class BaseVMFragment : BaseFragment { +abstract class BaseVMFragment : BaseFragment { constructor() : super() diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseVM.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseVM.kt deleted file mode 100644 index 69b4a8f5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseVM.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.Answer -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -abstract class BaseVM : ViewModel() { - - protected val tasksEventChannel = Channel() - val tasksEvent = tasksEventChannel.receiveAsFlow() - - protected fun makeJob( - job: suspend () -> Answer, - onAnswer: suspend (T) -> Unit = {}, - onStart: (suspend () -> Unit)? = null, - onEnd: (suspend () -> Unit)? = null, - onError: (suspend (String) -> Unit)? = null - ) = viewModelScope.launch { - onStart?.invoke() - when (val response = job()) { - is Answer.Success -> onAnswer(response.data) - is Answer.Error -> onError?.invoke(response.errorString) ?: tasksEventChannel.send( - ShowDialogInfoEvent(null, response.errorString) - ) - } - }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } - - protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt new file mode 100644 index 00000000..1a85538a --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt @@ -0,0 +1,57 @@ +package com.meloda.fast.base.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.meloda.fast.api.VKException +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.VKErrors +import com.meloda.fast.util.Utils +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +abstract class BaseViewModel : ViewModel() { + + protected val tasksEventChannel = Channel() + val tasksEvent = tasksEventChannel.receiveAsFlow() + + protected fun makeJob( + job: suspend () -> Answer, + onAnswer: suspend (T) -> Unit = {}, + onStart: (suspend () -> Unit)? = null, + onEnd: (suspend () -> Unit)? = null, + onError: (suspend (Throwable) -> Unit)? = null + ) = viewModelScope.launch { + onStart?.invoke() + when (val response = job()) { + is Answer.Success -> onAnswer(response.data) + is Answer.Error -> onError?.invoke(response.throwable) + } + }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } + + protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) + + protected suspend fun checkErrors(throwable: Throwable) { + // TODO: 8/31/2021 check illegal token + if (throwable is VKException) { + when (throwable.error) { + VKErrors.NEED_CAPTCHA -> { + throwable.captcha = + (throwable.json?.optString("captcha_sid") + ?: "") to (throwable.json?.optString("captcha_img") ?: "") + return + } + VKErrors.NEED_VALIDATION -> { + throwable.validationSid = throwable.json?.optString("validation_sid") + return + } + + + } + } + + tasksEventChannel.send(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable))) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/storage/ChatsStorage.kt b/app/src/main/kotlin/com/meloda/fast/database/storage/ChatsStorage.kt index 3db39d94..436e2842 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/storage/ChatsStorage.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/storage/ChatsStorage.kt @@ -26,7 +26,7 @@ import com.meloda.fast.database.DatabaseKeys.UNREAD_COUNT import com.meloda.fast.database.DatabaseUtils.TABLE_CHATS import com.meloda.fast.database.base.Storage import com.meloda.fast.api.model.VKConversation -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import org.json.JSONObject @WorkerThread diff --git a/app/src/main/kotlin/com/meloda/fast/database/storage/GroupsStorage.kt b/app/src/main/kotlin/com/meloda/fast/database/storage/GroupsStorage.kt index 4ba14d45..a7e80011 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/storage/GroupsStorage.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/storage/GroupsStorage.kt @@ -18,7 +18,7 @@ import com.meloda.fast.database.DatabaseKeys.TYPE import com.meloda.fast.database.DatabaseUtils.TABLE_GROUPS import com.meloda.fast.database.base.Storage import com.meloda.fast.api.model.VKGroup -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import org.json.JSONObject class GroupsStorage : Storage() { diff --git a/app/src/main/kotlin/com/meloda/fast/database/storage/UsersStorage.kt b/app/src/main/kotlin/com/meloda/fast/database/storage/UsersStorage.kt index 280d39f6..db83abaf 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/storage/UsersStorage.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/storage/UsersStorage.kt @@ -26,7 +26,7 @@ import com.meloda.fast.database.DatabaseUtils.TABLE_USERS import com.meloda.fast.database.QueryBuilder import com.meloda.fast.database.base.Storage import com.meloda.fast.api.model.VKUser -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import org.json.JSONObject @WorkerThread diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index f9577eba..0060cc8f 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -1,13 +1,11 @@ package com.meloda.fast.screens.login -import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Bundle import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding -import android.webkit.CookieManager import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -20,6 +18,7 @@ import coil.load import coil.transform.RoundedCornersTransformation import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout +import com.meloda.fast.BuildConfig import com.meloda.fast.R import com.meloda.fast.base.BaseVMFragment import com.meloda.fast.base.viewmodel.StartProgressEvent @@ -37,12 +36,12 @@ import java.util.* import kotlin.concurrent.schedule @AndroidEntryPoint -class LoginFragment : BaseVMFragment(R.layout.fragment_login) { +class LoginFragment : BaseVMFragment(R.layout.fragment_login) { - override val viewModel: LoginVM by viewModels() + override val viewModel: LoginViewModel by viewModels() private val binding: FragmentLoginBinding by viewBinding() - private var lastEmail: String = "" + private var lastLogin: String = "" private var lastPassword: String = "" private var errorTimer: Timer? = null @@ -72,9 +71,9 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { when (event) { is ShowError -> showErrorSnackbar(event.errorDescription) - is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid) - is GoToValidationEvent -> goToValidation(event.redirectUrl) - is GoToMainEvent -> goToMain(event.haveAuthorized) + is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second) + is ValidationRequired -> goToValidation() + is SuccessAuth -> goToMain(event.haveAuthorized) StartProgressEvent -> onProgressStarted() StopProgressEvent -> onProgressStopped() } @@ -95,28 +94,11 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { } private fun prepareViews() { - prepareWebView() prepareEmailEditText() preparePasswordEditText() prepareAuthButton() } - @SuppressLint("SetJavaScriptEnabled") - private fun prepareWebView() { - with(binding.webView) { - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.loadsImagesAutomatically = false - settings.userAgentString = "Chrome/41.0.2228.0 Safari/537.36" - clearCache(true) - } - - val cookieManager = CookieManager.getInstance() - cookieManager.removeAllCookies(null) - cookieManager.flush() - cookieManager.setAcceptCookie(false) - } - private fun prepareEmailEditText() { binding.loginInput.addTextChangedListener { if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = "" @@ -150,23 +132,30 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { } private fun prepareAuthButton() { - binding.auth.setOnClickListener { - if (binding.progress.isVisible) return@setOnClickListener + binding.auth.setOnClickListener { validateDataAndAuth() } + binding.auth.setOnLongClickListener { + validateDataAndAuth(BuildConfig.vkLogin to BuildConfig.vkPassword) + true + } + } - val loginString = binding.loginInput.text.toString().trim() - val passwordString = binding.passwordInput.text.toString().trim() + private fun validateDataAndAuth(data: Pair? = null) { + if (binding.progress.isVisible) return + val loginString = data?.first ?: binding.loginInput.text.toString().trim() + val passwordString = data?.second ?: binding.passwordInput.text.toString().trim() - if (!validateInputData(loginString, passwordString)) return@setOnClickListener + if (!validateInputData(loginString, passwordString)) return - KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) + lastLogin = loginString + lastPassword = passwordString - lifecycleScope.launch { - viewModel.login( - binding.webView, - loginString, - passwordString - ) - } + KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) + + lifecycleScope.launch { + viewModel.login( + login = loginString, + password = passwordString + ) } } @@ -220,7 +209,7 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { captchaInputLayout?.error = "" } - private fun showCaptchaDialog(captchaImage: String, captchaSid: String) { + private fun showCaptchaDialog(captchaSid: String, captchaImage: String) { val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false) captchaInputLayout = captchaBinding.captchaLayout @@ -250,11 +239,9 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { lifecycleScope.launch { viewModel.login( - webView = binding.webView, - email = lastEmail, + login = lastLogin, password = lastPassword, - captchaSid = captchaSid, - captchaKey = captchaCode + captcha = captchaSid to captchaCode ) } } @@ -272,11 +259,11 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { snackbar.show() } - private fun goToValidation(redirectUrl: String) { - findNavController().navigate( - R.id.toValidation, - bundleOf("redirectUrl" to redirectUrl) - ) + private fun goToValidation() { +// findNavController().navigate( +// R.id.toValidation, +// bundleOf("redirectUrl" to redirectUrl) +// ) } private fun goToMain(haveAuthorized: Boolean) { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginVM.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginVM.kt deleted file mode 100644 index f25ed0d9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginVM.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.meloda.fast.screens.login - -import android.os.Bundle -import android.util.Log -import android.webkit.JavascriptInterface -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.lifecycle.viewModelScope -import com.meloda.fast.UserConfig -import com.meloda.fast.api.VKAuth -import com.meloda.fast.base.viewmodel.BaseVM -import com.meloda.fast.base.viewmodel.StartProgressEvent -import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.json.JSONObject -import org.jsoup.Jsoup - -class LoginVM : BaseVM() { - - private var isWebViewPrepared = false - - suspend fun login( - webView: WebView, - email: String, - password: String, - captchaSid: String? = null, - captchaKey: String? = null - ) { - sendEvent(StartProgressEvent) - - val urlToGo = VKAuth.getDirectAuthUrl(email, password, captchaSid, captchaKey) - - if (!isWebViewPrepared) { - isWebViewPrepared = true - - webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler") - - webView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - webView.loadUrl( - "javascript:window.HtmlHandler.handleHtml" + - "(''+document.getElementsByTagName('html')[0].innerHTML+'');" - ) - } - } - } - - webView.loadUrl(urlToGo) - } - - @Suppress("MoveVariableDeclarationIntoWhen") - private fun checkResponse(response: JSONObject) { - viewModelScope.launch(Dispatchers.Default) { - if (response.has("error")) { - sendEvent(StopProgressEvent) - - val errorString = response.optString("error") - val errorDescription = response.optString("error_description") - - // TODO: 7/27/2021 use this with localized resources -// val errorType = response.optString("error_type") - - when (errorString) { - "need_validation" -> { - val redirectUrl = response.optString("redirect_uri") - - tasksEventChannel.send(GoToValidationEvent(redirectUrl)) - } - "need_captcha" -> { - val captchaImage = response.optString("captcha_img") - val captchaSid = response.optString("captcha_sid") - - Log.d("CAPTCHA", "captchaImage: $captchaImage") - - tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid)) - } - else -> { - tasksEventChannel.send(ShowError(errorDescription)) - } - } - } else { - delay(1500) - sendEvent(StopProgressEvent) - - val userId = response.optInt("user_id", -1) - val accessToken = response.optString("access_token") - - UserConfig.accessToken = accessToken - UserConfig.userId = userId - - tasksEventChannel.send(GoToMainEvent()) - } - } - } - - suspend fun getValidatedData(bundle: Bundle) { - val accessToken = bundle.getString("token") ?: "" - val userId = bundle.getInt("userId") - - UserConfig.accessToken = accessToken - UserConfig.userId = userId - - tasksEventChannel.send(GoToMainEvent()) - } - - inner class WebViewHandlerInterface { - @JavascriptInterface - fun handleHtml(html: String) { - val doc = Jsoup.parse(html) - - val responseString = - doc.select("pre[style=\"word-wrap: break-word; white-space: pre-wrap;\"]").first() - ?.text() ?: "" - - checkResponse(JSONObject(responseString)) - } - } - -} - -data class ShowError(val errorDescription: String) : VKEvent() -data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent() -data class GoToValidationEvent(val redirectUrl: String) : VKEvent() -data class GoToMainEvent(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt new file mode 100644 index 00000000..8a693924 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -0,0 +1,147 @@ +package com.meloda.fast.screens.login + +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.meloda.fast.UserConfig +import com.meloda.fast.api.VKAuth +import com.meloda.fast.api.VKConstants +import com.meloda.fast.api.VKException +import com.meloda.fast.api.VKUtil +import com.meloda.fast.api.network.repo.AuthRepo +import com.meloda.fast.api.network.request.RequestAuthDirect +import com.meloda.fast.base.viewmodel.BaseViewModel +import com.meloda.fast.base.viewmodel.StartProgressEvent +import com.meloda.fast.base.viewmodel.StopProgressEvent +import com.meloda.fast.base.viewmodel.VKEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.json.JSONObject +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val repo: AuthRepo +) : BaseViewModel() { + + suspend fun login( + login: String, + password: String, + twoFa: Boolean = false, + twoFaCode: String? = null, + captcha: Pair? = null + ) { + makeJob( + { + repo.auth( + RequestAuthDirect( + grantType = VKAuth.GrantType.PASSWORD, + clientId = VKConstants.VK_APP_ID, + clientSecret = VKConstants.VK_SECRET, + username = login, + password = password, + scope = VKAuth.scope, + twoFaForceSms = twoFa, + twoFaCode = twoFaCode, + captchaSid = captcha?.first, + captchaKey = captcha?.second + ).map + ) + }, + onAnswer = { + // TODO: 8/31/2021 do something + if (it.userId == null || it.accessToken == null) { + return@makeJob + } + + UserConfig.userId = it.userId + UserConfig.accessToken = it.accessToken + + sendEvent(SuccessAuth(haveAuthorized = true)) + }, + onError = { + checkErrors(it) + + if (it !is VKException) return@makeJob + + if (VKUtil.isValidationRequired(it)) { + sendEvent(ValidationRequired(validationSid = it.validationSid)) + } else if (VKUtil.isCaptchaRequired(it) && it.captcha != null) { + sendEvent(CaptchaRequired(it.captcha!!.first to it.captcha!!.second)) + } + }, + onStart = { sendEvent(StartProgressEvent) }, + onEnd = { sendEvent(StopProgressEvent) } + ) + } + + @Suppress("MoveVariableDeclarationIntoWhen") + private fun checkResponse(response: JSONObject) { + viewModelScope.launch(Dispatchers.Default) { + if (response.has("error")) { + sendEvent(StopProgressEvent) + + val errorString = response.optString("error") + val errorDescription = response.optString("error_description") + + // TODO: 7/27/2021 use this with localized resources +// val errorType = response.optString("error_type") + + when (errorString) { + "need_validation" -> { + val redirectUrl = response.optString("redirect_uri") + + tasksEventChannel.send(ValidationRequired(redirectUrl)) + } + "need_captcha" -> { + val captchaImage = response.optString("captcha_img") + val captchaSid = response.optString("captcha_sid") + + Log.d("CAPTCHA", "captchaImage: $captchaImage") + + tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid)) + } + else -> { + tasksEventChannel.send(ShowError(errorDescription)) + } + } + } else { + delay(1500) + sendEvent(StopProgressEvent) + + val userId = response.optInt("user_id", -1) + val accessToken = response.optString("access_token") + + UserConfig.accessToken = accessToken + UserConfig.userId = userId + + tasksEventChannel.send(SuccessAuth()) + } + } + } + + suspend fun getValidatedData(bundle: Bundle) { + val accessToken = bundle.getString("token") ?: "" + val userId = bundle.getInt("userId") + + UserConfig.accessToken = accessToken + UserConfig.userId = userId + + tasksEventChannel.send(SuccessAuth()) + } + +} + +data class ShowError(val errorDescription: String) : VKEvent() +data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent() + +data class ValidationRequired( + val redirectUrl: String? = null, + val validationSid: String? = null +) : VKEvent() + +data class CaptchaRequired(val captcha: Pair) : VKEvent() + +data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt index 7a697e2c..1d024439 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt @@ -13,9 +13,9 @@ import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainFragment : BaseVMFragment(R.layout.fragment_main) { +class MainFragment : BaseVMFragment(R.layout.fragment_main) { - override val viewModel: MainVM by viewModels() + override val viewModel: MainViewModel by viewModels() private val binding: FragmentMainBinding by viewBinding() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainVM.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainVM.kt deleted file mode 100644 index 39b40f31..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainVM.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.meloda.fast.screens.main - -import com.meloda.fast.base.viewmodel.BaseVM - -class MainVM : BaseVM() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt new file mode 100644 index 00000000..af5c0d73 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt @@ -0,0 +1,5 @@ +package com.meloda.fast.screens.main + +import com.meloda.fast.base.viewmodel.BaseViewModel + +class MainViewModel : BaseViewModel() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsFragment.kt index 523359c3..f1b6f817 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsFragment.kt @@ -16,9 +16,9 @@ import dagger.hilt.android.AndroidEntryPoint import kotlin.math.roundToInt @AndroidEntryPoint -class ConversationsFragment : BaseVMFragment(R.layout.fragment_conversations) { +class ConversationsFragment : BaseVMFragment(R.layout.fragment_conversations) { - override val viewModel: ConversationsVM by viewModels() + override val viewModel: ConversationsViewModel by viewModels() private val binding: FragmentConversationsBinding by viewBinding() private lateinit var adapter: ConversationsAdapter diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsVM.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsViewModel.kt similarity index 70% rename from app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsVM.kt rename to app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsViewModel.kt index e9479a86..035567d5 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsVM.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/ConversationsViewModel.kt @@ -1,11 +1,11 @@ package com.meloda.fast.screens.messages import androidx.lifecycle.viewModelScope -import com.meloda.fast.base.viewmodel.BaseVM +import com.meloda.fast.base.viewmodel.BaseViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class ConversationsVM : BaseVM() { +class ConversationsViewModel : BaseViewModel() { fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { diff --git a/app/src/main/kotlin/com/meloda/fast/util/VKUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/VKUtils.kt index b97e060f..04782be7 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/VKUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/util/VKUtils.kt @@ -13,7 +13,7 @@ import com.meloda.fast.R import com.meloda.fast.api.model.* import com.meloda.fast.common.AppGlobal import com.meloda.fast.api.OnResponseListener -import com.meloda.fast.api.util.VKUtil +import com.meloda.fast.api.VKUtil import java.text.SimpleDateFormat import java.util.* import kotlin.math.abs diff --git a/app/src/main/kotlin/com/meloda/fast/util/ViewUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ViewUtils.kt index 68a91afd..16b440c9 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/ViewUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/util/ViewUtils.kt @@ -11,7 +11,6 @@ import com.meloda.fast.extensions.ContextExtensions.color import com.meloda.fast.R import com.meloda.fast.widget.CircleImageView import com.meloda.fast.api.model.VKUser -import com.meloda.fast.api.util.VKUtil object ViewUtils { diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml index cbdb0817..4d407940 100644 --- a/app/src/main/res/layout/dialog_captcha.xml +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -12,7 +12,8 @@ + android:textColor="?colorAction" + app:elevation="0dp" + app:rippleColor="?colorActionRipple" /> + android:textColor="?colorActionContentPrimary" + app:elevation="0dp" + app:rippleColor="?colorActionRipple" /> diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index ecec7bc7..f418a5ca 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -9,12 +9,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - Date: Tue, 31 Aug 2021 15:51:42 +0300 Subject: [PATCH 2/2] 2fa support --- .../kotlin/com/meloda/fast/api/VKException.kt | 5 +- .../com/meloda/fast/api/network/VKUrls.kt | 9 +- .../api/network/datasource/AuthDataSource.kt | 2 + .../meloda/fast/api/network/repo/AuthRepo.kt | 4 + .../fast/api/network/response/AuthResponse.kt | 8 ++ .../fast/screens/login/LoginFragment.kt | 98 ++++++++++++------- .../fast/screens/login/LoginViewModel.kt | 78 ++++----------- app/src/main/res/layout/dialog_validation.xml | 83 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 9 files changed, 192 insertions(+), 97 deletions(-) create mode 100644 app/src/main/res/layout/dialog_validation.xml diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt index 1044a675..cfb87091 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt @@ -7,14 +7,11 @@ class VKException(var url: String = "", var description: String = "", var error: IOException(description) { var captcha: Pair? = null - var redirectUri: String? = null - var validationSid: String? = null - var json: JSONObject? = null override fun toString(): String { - return "url: $url;\n\nerror: $error; description: $description;" + return "error: $error; description: $description;" } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt index 5075877b..fd2ab868 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt @@ -2,16 +2,19 @@ package com.meloda.fast.api.network object VKUrls { + const val OAUTH = "https://oauth.vk.com" + const val API = "https://api.vk.com/method" + object Auth { - const val directAuth = "https://oauth.vk.com/token" + const val directAuth = "$OAUTH/token" + const val sendSms = "$API/auth.validatePhone" } object Conversations { - const val get = "messages.getConversations" + const val get = "$API/messages.getConversations" } - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt index 4c9a06fb..68c5d6bc 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt @@ -7,4 +7,6 @@ class AuthDataSource @Inject constructor( private val repo: AuthRepo ) : AuthRepo { override suspend fun auth(param: Map) = repo.auth(param) + + override suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt index f05377ef..93c92fa7 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt @@ -3,6 +3,7 @@ package com.meloda.fast.api.network.repo import com.meloda.fast.api.network.VKUrls import com.meloda.fast.api.network.response.ResponseAuthDirect import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.response.ResponseSendSms import retrofit2.http.* interface AuthRepo { @@ -10,4 +11,7 @@ interface AuthRepo { @GET(VKUrls.Auth.directAuth) suspend fun auth(@QueryMap param: Map): Answer + @GET(VKUrls.Auth.sendSms) + suspend fun sendSms(@Query("sid") validationSid: String): Answer + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt index dded6222..e9fe49b3 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt @@ -10,4 +10,12 @@ data class ResponseAuthDirect( @SerializedName("user_id") val userId: Int? = null, @SerializedName("trusted_hash") val twoFaHash: String? = null, @SerializedName("validation_sid") val validationSid: String? = null +) : Parcelable + +@Parcelize +data class ResponseSendSms( + @SerializedName("sid") val validationSid: String?, + @SerializedName("delay") val delay: Int?, + @SerializedName("validation_type") val validationType: String?, + @SerializedName("validation_resend") val validationResend: String? ) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index 0060cc8f..653d8e33 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.setFragmentResultListener @@ -25,6 +24,7 @@ import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.databinding.DialogCaptchaBinding +import com.meloda.fast.databinding.DialogValidationBinding import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.screens.main.MainFragment import com.meloda.fast.util.KeyboardUtils @@ -45,7 +45,9 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { private var lastPassword: String = "" private var errorTimer: Timer? = null + private var captchaInputLayout: TextInputLayout? = null + private var validationInputLayout: TextInputLayout? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -59,11 +61,6 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { setFragmentResultListener("validation") { _, bundle -> lifecycleScope.launch { viewModel.getValidatedData(bundle) } } - -// showCaptchaDialog( -// "https://www.vets4pets.com/syssiteassets/species/cat/kitten/tiny-kitten-in-field.jpg?width=1040", -// "" -// ) } override fun onEvent(event: VKEvent) { @@ -72,8 +69,12 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { when (event) { is ShowError -> showErrorSnackbar(event.errorDescription) is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second) - is ValidationRequired -> goToValidation() + + CodeSent -> showValidationDialog() + + is ValidationRequired -> showValidationRequired() is SuccessAuth -> goToMain(event.haveAuthorized) + StartProgressEvent -> onProgressStarted() StopProgressEvent -> onProgressStopped() } @@ -151,19 +152,19 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) - lifecycleScope.launch { - viewModel.login( - login = loginString, - password = passwordString - ) - } + + viewModel.login( + login = loginString, + password = passwordString + ) } // TODO: 7/27/2021 extract strings to resources private fun validateInputData( loginString: String?, passwordString: String?, - captchaCode: String? = null + captchaCode: String? = null, + validationCode: String? = null ): Boolean { var isValidated = true @@ -182,6 +183,11 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { setError("Input code", captchaInputLayout!!) } + if (validationCode?.isEmpty() == true && validationInputLayout != null) { + isValidated = false + setError("Input code", validationInputLayout!!) + } + return isValidated } @@ -237,17 +243,52 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { dialog.dismiss() - lifecycleScope.launch { - viewModel.login( - login = lastLogin, - password = lastPassword, - captcha = captchaSid to captchaCode - ) - } + viewModel.login( + login = lastLogin, + password = lastPassword, + captcha = captchaSid to captchaCode + ) } captchaBinding.cancel.setOnClickListener { dialog.dismiss() } } + private fun showValidationDialog() { + val validationBinding = DialogValidationBinding.inflate(layoutInflater, null, false) + validationInputLayout = validationBinding.codeLayout + + val builder = AlertDialog.Builder(requireContext()) + .setView(validationBinding.root) + .setCancelable(false) + .setTitle(R.string.input_validation_code) + + val dialog = builder.show() + + validationBinding.ok.setOnClickListener { + val validationCode = validationBinding.codeInput.text.toString().trim() + + if (!validateInputData( + loginString = null, + passwordString = null, + validationCode = validationCode + ) + ) return@setOnClickListener + + dialog.dismiss() + + viewModel.login( + login = lastLogin, + password = lastPassword, + twoFaCode = validationCode + ) + } + validationBinding.cancel.setOnClickListener { dialog.dismiss() } + } + + // TODO: 8/31/2021 show snackbar + private fun showValidationRequired() { + + } + private fun showErrorSnackbar(errorDescription: String) { val snackbar = Snackbar.make( requireView(), @@ -259,19 +300,10 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { snackbar.show() } - private fun goToValidation() { -// findNavController().navigate( -// R.id.toValidation, -// bundleOf("redirectUrl" to redirectUrl) -// ) - } + private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch { + if (haveAuthorized) delay(500) - private fun goToMain(haveAuthorized: Boolean) { - lifecycleScope.launch { - if (haveAuthorized) delay(500) - - findNavController().navigate(R.id.toMain) - } + findNavController().navigate(R.id.toMain) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index 8a693924..afe5c5b5 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -26,13 +26,12 @@ class LoginViewModel @Inject constructor( private val repo: AuthRepo ) : BaseViewModel() { - suspend fun login( + fun login( login: String, password: String, - twoFa: Boolean = false, twoFaCode: String? = null, captcha: Pair? = null - ) { + ) = viewModelScope.launch { makeJob( { repo.auth( @@ -43,7 +42,7 @@ class LoginViewModel @Inject constructor( username = login, password = password, scope = VKAuth.scope, - twoFaForceSms = twoFa, + twoFaForceSms = true, twoFaCode = twoFaCode, captchaSid = captcha?.first, captchaKey = captcha?.second @@ -63,13 +62,18 @@ class LoginViewModel @Inject constructor( }, onError = { checkErrors(it) - if (it !is VKException) return@makeJob if (VKUtil.isValidationRequired(it)) { - sendEvent(ValidationRequired(validationSid = it.validationSid)) - } else if (VKUtil.isCaptchaRequired(it) && it.captcha != null) { - sendEvent(CaptchaRequired(it.captcha!!.first to it.captcha!!.second)) + it.validationSid?.let { sid -> + sendEvent(ValidationRequired(validationSid = sid)) + + sendSms(sid) + } + } else if (VKUtil.isCaptchaRequired(it)) { + it.captcha?.let { captcha -> + sendEvent(CaptchaRequired(captcha.first to captcha.second)) + } } }, onStart = { sendEvent(StartProgressEvent) }, @@ -77,49 +81,12 @@ class LoginViewModel @Inject constructor( ) } - @Suppress("MoveVariableDeclarationIntoWhen") - private fun checkResponse(response: JSONObject) { - viewModelScope.launch(Dispatchers.Default) { - if (response.has("error")) { - sendEvent(StopProgressEvent) - - val errorString = response.optString("error") - val errorDescription = response.optString("error_description") - - // TODO: 7/27/2021 use this with localized resources -// val errorType = response.optString("error_type") - - when (errorString) { - "need_validation" -> { - val redirectUrl = response.optString("redirect_uri") - - tasksEventChannel.send(ValidationRequired(redirectUrl)) - } - "need_captcha" -> { - val captchaImage = response.optString("captcha_img") - val captchaSid = response.optString("captcha_sid") - - Log.d("CAPTCHA", "captchaImage: $captchaImage") - - tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid)) - } - else -> { - tasksEventChannel.send(ShowError(errorDescription)) - } - } - } else { - delay(1500) - sendEvent(StopProgressEvent) - - val userId = response.optInt("user_id", -1) - val accessToken = response.optString("access_token") - - UserConfig.accessToken = accessToken - UserConfig.userId = userId - - tasksEventChannel.send(SuccessAuth()) - } - } + fun sendSms(validationSid: String) = viewModelScope.launch { + makeJob({ repo.sendSms(validationSid) }, + onAnswer = { sendEvent(CodeSent) }, + onError = {}, + onStart = {}, + onEnd = {}) } suspend fun getValidatedData(bundle: Bundle) { @@ -135,13 +102,10 @@ class LoginViewModel @Inject constructor( } data class ShowError(val errorDescription: String) : VKEvent() -data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent() - -data class ValidationRequired( - val redirectUrl: String? = null, - val validationSid: String? = null -) : VKEvent() +data class ValidationRequired(val validationSid: String) : VKEvent() data class CaptchaRequired(val captcha: Pair) : VKEvent() +object CodeSent : VKEvent() + data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_validation.xml b/app/src/main/res/layout/dialog_validation.xml new file mode 100644 index 00000000..9e24ba07 --- /dev/null +++ b/app/src/main/res/layout/dialog_validation.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a35b892..a2e533dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,5 +156,7 @@ Login Conversations + Code + Input code from sms