forked from melod1n/fast-messenger
Merge pull request #4 from melod1n/2fa_direct_support
2fa direct support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ object UserConfig {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean {
|
||||
return userId > 0 && !TextUtils.isEmpty(accessToken)
|
||||
}
|
||||
fun isLoggedIn() = userId > 0 && accessToken.isNotBlank()
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
sealed class Answer<out R> {
|
||||
data class Success<out T>(val data: T) : Answer<T>()
|
||||
data class Error(val errorString: String) : Answer<Nothing>()
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
data class Resource<out T> constructor(
|
||||
val status: Status,
|
||||
val responseData: T?,
|
||||
val message: String?
|
||||
) {
|
||||
|
||||
enum class Status {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
LOADING
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> success(responseData: T?): Resource<T> =
|
||||
Resource(Status.SUCCESS, responseData, null)
|
||||
|
||||
fun <T> error(message: String?, responseBody: T? = null): Resource<T> =
|
||||
Resource(Status.ERROR, responseBody, message)
|
||||
|
||||
fun <T> loading(responseData: T? = null): Resource<T> =
|
||||
Resource(Status.LOADING, responseData, null)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String>? = 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}&" +
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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
|
||||
var redirectUri: String? = null
|
||||
class VKException(var url: String = "", var description: String = "", var error: String) :
|
||||
IOException(description) {
|
||||
|
||||
var captcha: Pair<String, String>? = null
|
||||
var validationSid: String? = null
|
||||
var json: JSONObject? = null
|
||||
|
||||
override fun toString(): String {
|
||||
return "code: $code, message: $message"
|
||||
return "error: $error; description: $description;"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<GetConversationsResponse>
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
object VKUrls {
|
||||
|
||||
const val getConversations = "messages.getConversations"
|
||||
|
||||
}
|
||||
+17
-1
@@ -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)
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.meloda.fast.api.datasource
|
||||
|
||||
class MessagesDataSource constructor() {
|
||||
}
|
||||
@@ -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 <T> getResult(apiCall: suspend () -> ApiResponse<T>): Resource<T> {
|
||||
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<T>(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 <T> 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
data class ApiResponse<T> constructor(
|
||||
val isSuccessful: Boolean,
|
||||
val error: Error?,
|
||||
val response: T?
|
||||
)
|
||||
|
||||
data class Error constructor(
|
||||
val code: Long,
|
||||
val message: String
|
||||
)
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -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"
|
||||
|
||||
}
|
||||
@@ -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<out Annotation>,
|
||||
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<Any?>(resultInnerType)
|
||||
}
|
||||
return ResultCallAdapter<Nothing>(Nothing::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : Call<Out> {
|
||||
|
||||
override fun execute(): Response<Out> = throw NotImplementedError()
|
||||
|
||||
final override fun enqueue(callback: Callback<Out>) = enqueueImpl(callback)
|
||||
|
||||
final override fun clone(): Call<Out> = 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<Out>)
|
||||
|
||||
abstract fun cloneImpl(): Call<Out>
|
||||
}
|
||||
|
||||
private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<Answer<R>>> {
|
||||
|
||||
override fun responseType() = type
|
||||
|
||||
override fun adapt(call: Call<R>): Call<Answer<R>> = ResultCall(call)
|
||||
}
|
||||
|
||||
internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy) {
|
||||
|
||||
override fun enqueueImpl(callback: Callback<Answer<T>>) {
|
||||
proxy.enqueue(ResultCallback(this, callback))
|
||||
}
|
||||
|
||||
override fun cloneImpl(): ResultCall<T> {
|
||||
return ResultCall(proxy.clone())
|
||||
}
|
||||
|
||||
private class ResultCallback<T>(
|
||||
private val proxy: ResultCall<T>,
|
||||
private val callback: Callback<Answer<T>>
|
||||
) : Callback<T> {
|
||||
|
||||
// TODO: 8/31/2021 parse VK errors
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val result: Answer<T> = 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<T>, error: Throwable) {
|
||||
callback.onResponse(
|
||||
proxy,
|
||||
Response.success(Answer.Error(throwable = error))
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkErrors(call: Call<T>, 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<out R> {
|
||||
|
||||
data class Success<out T>(val data: T) : Answer<T>()
|
||||
data class Error(val throwable: Throwable) : Answer<Nothing>()
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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 = "$OAUTH/token"
|
||||
const val sendSms = "$API/auth.validatePhone"
|
||||
}
|
||||
|
||||
object Conversations {
|
||||
const val get = "$API/messages.getConversations"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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<String, String?>) = repo.auth(param)
|
||||
|
||||
override suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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 {
|
||||
|
||||
@GET(VKUrls.Auth.directAuth)
|
||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||
|
||||
@GET(VKUrls.Auth.sendSms)
|
||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||
|
||||
}
|
||||
@@ -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<GetConversationsResponse>
|
||||
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.meloda.fast.api.network.request
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.meloda.fast.api.model.request
|
||||
package com.meloda.fast.api.network.request
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
|
||||
@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
|
||||
+1
-1
@@ -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
|
||||
@@ -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<Map<String, Any>>
|
||||
|
||||
}
|
||||
@@ -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<VM : BaseVM> : BaseFragment {
|
||||
abstract class BaseVMFragment<VM : BaseViewModel> : BaseFragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
|
||||
@@ -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<VKEvent>()
|
||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||
|
||||
protected fun <T> makeJob(
|
||||
job: suspend () -> Answer<T>,
|
||||
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 <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
||||
|
||||
}
|
||||
@@ -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<VKEvent>()
|
||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||
|
||||
protected fun <T> makeJob(
|
||||
job: suspend () -> Answer<T>,
|
||||
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 <T : VKEvent> 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)))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<VKGroup>() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
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
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
@@ -20,12 +17,14 @@ 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
|
||||
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
|
||||
@@ -37,16 +36,18 @@ import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
class LoginFragment : BaseVMFragment<LoginViewModel>(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
|
||||
|
||||
private var captchaInputLayout: TextInputLayout? = null
|
||||
private var validationInputLayout: TextInputLayout? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -60,11 +61,6 @@ class LoginFragment : BaseVMFragment<LoginVM>(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,9 +68,13 @@ class LoginFragment : BaseVMFragment<LoginVM>(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)
|
||||
|
||||
CodeSent -> showValidationDialog()
|
||||
|
||||
is ValidationRequired -> showValidationRequired()
|
||||
is SuccessAuth -> goToMain(event.haveAuthorized)
|
||||
|
||||
StartProgressEvent -> onProgressStarted()
|
||||
StopProgressEvent -> onProgressStopped()
|
||||
}
|
||||
@@ -95,28 +95,11 @@ class LoginFragment : BaseVMFragment<LoginVM>(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,31 +133,38 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
}
|
||||
|
||||
private fun prepareAuthButton() {
|
||||
binding.auth.setOnClickListener {
|
||||
if (binding.progress.isVisible) return@setOnClickListener
|
||||
|
||||
val loginString = binding.loginInput.text.toString().trim()
|
||||
val passwordString = binding.passwordInput.text.toString().trim()
|
||||
|
||||
if (!validateInputData(loginString, passwordString)) return@setOnClickListener
|
||||
|
||||
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.login(
|
||||
binding.webView,
|
||||
loginString,
|
||||
passwordString
|
||||
)
|
||||
}
|
||||
binding.auth.setOnClickListener { validateDataAndAuth() }
|
||||
binding.auth.setOnLongClickListener {
|
||||
validateDataAndAuth(BuildConfig.vkLogin to BuildConfig.vkPassword)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDataAndAuth(data: Pair<String, String>? = 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
|
||||
|
||||
lastLogin = loginString
|
||||
lastPassword = passwordString
|
||||
|
||||
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -193,6 +183,11 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
setError("Input code", captchaInputLayout!!)
|
||||
}
|
||||
|
||||
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
||||
isValidated = false
|
||||
setError("Input code", validationInputLayout!!)
|
||||
}
|
||||
|
||||
return isValidated
|
||||
}
|
||||
|
||||
@@ -220,7 +215,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(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
|
||||
|
||||
@@ -248,19 +243,52 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.login(
|
||||
webView = binding.webView,
|
||||
email = lastEmail,
|
||||
password = lastPassword,
|
||||
captchaSid = captchaSid,
|
||||
captchaKey = 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(),
|
||||
@@ -272,19 +300,10 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun goToValidation(redirectUrl: String) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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" +
|
||||
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,111 @@
|
||||
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() {
|
||||
|
||||
fun login(
|
||||
login: String,
|
||||
password: String,
|
||||
twoFaCode: String? = null,
|
||||
captcha: Pair<String, String>? = null
|
||||
) = viewModelScope.launch {
|
||||
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 = true,
|
||||
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)) {
|
||||
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) },
|
||||
onEnd = { sendEvent(StopProgressEvent) }
|
||||
)
|
||||
}
|
||||
|
||||
fun sendSms(validationSid: String) = viewModelScope.launch {
|
||||
makeJob({ repo.sendSms(validationSid) },
|
||||
onAnswer = { sendEvent(CodeSent) },
|
||||
onError = {},
|
||||
onStart = {},
|
||||
onEnd = {})
|
||||
}
|
||||
|
||||
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 ValidationRequired(val validationSid: String) : VKEvent()
|
||||
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
|
||||
|
||||
object CodeSent : VKEvent()
|
||||
|
||||
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
|
||||
@@ -13,9 +13,9 @@ import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainFragment : BaseVMFragment<MainVM>(R.layout.fragment_main) {
|
||||
class MainFragment : BaseVMFragment<MainViewModel>(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?) {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.meloda.fast.screens.main
|
||||
|
||||
import com.meloda.fast.base.viewmodel.BaseVM
|
||||
|
||||
class MainVM : BaseVM()
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.meloda.fast.screens.main
|
||||
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
@@ -16,9 +16,9 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConversationsFragment : BaseVMFragment<ConversationsVM>(R.layout.fragment_conversations) {
|
||||
class ConversationsFragment : BaseVMFragment<ConversationsViewModel>(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
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -65,7 +66,9 @@
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@android:string/cancel"
|
||||
app:elevation="0dp" />
|
||||
android:textColor="?colorAction"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
@@ -73,8 +76,11 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="?colorAction"
|
||||
android:text="@android:string/ok"
|
||||
app:elevation="0dp" />
|
||||
android:textColor="?colorActionContentPrimary"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/codeContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/codeImage"
|
||||
style="@style/AppTheme.Login.EditText.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/codeLayout"
|
||||
style="@style/Widget.TextInputLayout.NoError.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/codeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/code_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="number"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:text="@android:string/cancel"
|
||||
android:textColor="?colorAction"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="?colorAction"
|
||||
android:text="@android:string/ok"
|
||||
android:textColor="?colorActionContentPrimary"
|
||||
app:elevation="0dp"
|
||||
app:rippleColor="?colorActionRipple" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -9,12 +9,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -156,5 +156,7 @@
|
||||
<string name="login_hint">Login</string>
|
||||
|
||||
<string name="conversations">Conversations</string>
|
||||
<string name="code_hint">Code</string>
|
||||
<string name="input_validation_code">Input code from sms</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user