Merge pull request #4 from melod1n/2fa_direct_support

2fa direct support
This commit is contained in:
2021-08-31 15:52:30 +03:00
committed by GitHub
50 changed files with 789 additions and 481 deletions
+9
View File
@@ -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 { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -26,6 +31,10 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") {
buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password)
}
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
@@ -29,7 +29,6 @@ object UserConfig {
userId = -1 userId = -1
} }
fun isLoggedIn(): Boolean { fun isLoggedIn() = userId > 0 && accessToken.isNotBlank()
return userId > 0 && !TextUtils.isEmpty(accessToken)
}
} }
@@ -6,7 +6,7 @@ import com.meloda.fast.concurrent.EventInfo
import com.meloda.fast.concurrent.TaskManager import com.meloda.fast.concurrent.TaskManager
import com.meloda.fast.api.VKApiKeys import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.VKMessage import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.util.VKUtil import com.meloda.fast.api.VKUtil
import org.json.JSONArray import org.json.JSONArray
@Suppress("UNCHECKED_CAST") @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.MessageMethodSetter
import com.meloda.fast.api.method.MethodSetter import com.meloda.fast.api.method.MethodSetter
import com.meloda.fast.api.method.UserMethodSetter import com.meloda.fast.api.method.UserMethodSetter
import com.meloda.fast.api.network.ErrorCodes
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import org.json.JSONArray import org.json.JSONArray
@@ -55,13 +56,14 @@ object VKApi {
try { try {
checkError(json, url) checkError(json, url)
} catch (ex: VKException) { } catch (ex: VKException) {
if (ex.code == ErrorCodes.TOO_MANY_REQUESTS) { throw ex
Timer().schedule(object : TimerTask() { // if (ex.code == ErrorCodes.TOO_MANY_REQUESTS) {
override fun run() { // Timer().schedule(object : TimerTask() {
execute(url, cls) // override fun run() {
} // execute(url, cls)
}, 1000) // }
} else throw ex // }, 1000)
// } else throw ex
} }
when (cls) { when (cls) {
@@ -282,7 +284,7 @@ object VKApi {
val code = error.optInt("error_code", -1) val code = error.optInt("error_code", -1)
val message = error.optString("error_msg", "") val message = error.optString("error_msg", "")
val e = VKException(url, message, code) // val e = VKException(url, message, code)
//TODO: add checking invalid session //TODO: add checking invalid session
if (code == 5 && message.contains("invalid session")) { if (code == 5 && message.contains("invalid session")) {
@@ -291,16 +293,16 @@ object VKApi {
// }) // })
} }
if (code == ErrorCodes.CAPTCHA_NEEDED) { // if (code == ErrorCodes.CAPTCHA_NEEDED) {
e.captchaImg = error.optString("captcha_img") // e.captchaImg = error.optString("captcha_img")
e.captchaSid = error.optString("captcha_sid") // e.captchaSid = error.optString("captcha_sid")
} // }
//
// if (code == ErrorCodes.VALIDATION_REQUIRED) {
// e.redirectUri = error.optString("redirect_uri")
// }
if (code == ErrorCodes.VALIDATION_REQUIRED) { // throw e
e.redirectUri = error.optString("redirect_uri")
}
throw e
} }
} }
@@ -3,14 +3,17 @@ package com.meloda.fast.api
import android.util.Log import android.util.Log
import com.meloda.fast.BuildConfig import com.meloda.fast.BuildConfig
import com.meloda.fast.UserConfig import com.meloda.fast.UserConfig
import com.meloda.fast.api.util.VKUtil
import java.net.URLEncoder import java.net.URLEncoder
object VKAuth { object VKAuth {
private const val TAG = "VKM.VKAuth" private const val TAG = "VKM.VKAuth"
private const val settings = "notify," + object GrantType {
const val PASSWORD = "password"
}
const val scope = "notify," +
"friends," + "friends," +
"photos," + "photos," +
"audio," + "audio," +
@@ -30,17 +33,25 @@ object VKAuth {
fun getDirectAuthUrl( fun getDirectAuthUrl(
login: String, login: String,
password: String, password: String,
captchaSid: String? = null, twoFa: Boolean = false,
captchaKey: String? = null twoFaCode: String = "",
) = "https://oauth.vk.com/token?grant_type=password&" + captcha: Pair<String, String>? = null
) = "https://oauth.vk.com/token?" +
"grant_type=password&" +
"client_id=${VKConstants.VK_APP_ID}&" + "client_id=${VKConstants.VK_APP_ID}&" +
"scope=$settings&" +
"client_secret=${VKConstants.VK_SECRET}&" + "client_secret=${VKConstants.VK_SECRET}&" +
"username=$login&" + "username=$login&" +
"password=$password" + "password=$password&" +
(if (captchaSid == null || captchaKey == null) "" else "&captcha_sid=$captchaSid&captcha_key=$captchaKey") + "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")}" "&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?" + fun getOAuthUrl(settings: String) = "https://oauth.vk.com/authorize?" +
"client_id=${UserConfig.FAST_APP_ID}&" + "client_id=${UserConfig.FAST_APP_ID}&" +
@@ -1,15 +1,17 @@
package com.meloda.fast.api package com.meloda.fast.api
import org.json.JSONObject
import java.io.IOException import java.io.IOException
class VKException(var url: String = "", override var message: String = "", var code: Int) : class VKException(var url: String = "", var description: String = "", var error: String) :
IOException(message) { IOException(description) {
var captchaSid: String? = null
var captchaImg: String? = null var captcha: Pair<String, String>? = null
var redirectUri: String? = null var validationSid: String? = null
var json: JSONObject? = null
override fun toString(): String { 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"
}
@@ -1,7 +1,8 @@
package com.meloda.fast.api.util package com.meloda.fast.api
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.meloda.fast.api.model.* import com.meloda.fast.api.model.*
import com.meloda.fast.api.network.VKErrors
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -12,6 +13,21 @@ object VKUtil {
private const val TAG = "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? { fun extractPattern(string: String, pattern: String): String? {
val p = Pattern.compile(pattern) val p = Pattern.compile(pattern)
val m = p.matcher(string) 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 package com.meloda.fast.api.model
import android.util.ArrayMap import android.util.ArrayMap
import com.meloda.fast.api.util.VKUtil import com.meloda.fast.api.VKUtil
import org.json.JSONObject import org.json.JSONObject
open class VKMessage() : VKModel() { 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())
}
}
@@ -1,4 +1,4 @@
package com.meloda.fast.api package com.meloda.fast.api.network
object ErrorCodes { object ErrorCodes {
const val UNKNOWN_ERROR = 1 const val UNKNOWN_ERROR = 1
@@ -40,3 +40,12 @@ object ErrorCodes {
const val INVALID_DOC_TITLE = 1152 const val INVALID_DOC_TITLE = 1152
const val ACCESS_TO_DOC_DENIED = 1153 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,4 +1,4 @@
package com.meloda.fast.api.model.request package com.meloda.fast.api.network.request
import com.google.gson.annotations.SerializedName 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,4 +1,4 @@
package com.meloda.fast.api.model.response package com.meloda.fast.api.network.response
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize 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 android.view.View
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope 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 com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
abstract class BaseVMFragment<VM : BaseVM> : BaseFragment { abstract class BaseVMFragment<VM : BaseViewModel> : BaseFragment {
constructor() : super() 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.DatabaseUtils.TABLE_CHATS
import com.meloda.fast.database.base.Storage import com.meloda.fast.database.base.Storage
import com.meloda.fast.api.model.VKConversation import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.util.VKUtil import com.meloda.fast.api.VKUtil
import org.json.JSONObject import org.json.JSONObject
@WorkerThread @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.DatabaseUtils.TABLE_GROUPS
import com.meloda.fast.database.base.Storage import com.meloda.fast.database.base.Storage
import com.meloda.fast.api.model.VKGroup import com.meloda.fast.api.model.VKGroup
import com.meloda.fast.api.util.VKUtil import com.meloda.fast.api.VKUtil
import org.json.JSONObject import org.json.JSONObject
class GroupsStorage : Storage<VKGroup>() { 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.QueryBuilder
import com.meloda.fast.database.base.Storage import com.meloda.fast.database.base.Storage
import com.meloda.fast.api.model.VKUser import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil import com.meloda.fast.api.VKUtil
import org.json.JSONObject import org.json.JSONObject
@WorkerThread @WorkerThread
@@ -1,15 +1,12 @@
package com.meloda.fast.screens.login package com.meloda.fast.screens.login
import android.annotation.SuppressLint
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
@@ -20,12 +17,14 @@ import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.BuildConfig
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.base.BaseVMFragment import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.DialogCaptchaBinding import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.DialogValidationBinding
import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.screens.main.MainFragment import com.meloda.fast.screens.main.MainFragment
import com.meloda.fast.util.KeyboardUtils import com.meloda.fast.util.KeyboardUtils
@@ -37,16 +36,18 @@ import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@AndroidEntryPoint @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 val binding: FragmentLoginBinding by viewBinding()
private var lastEmail: String = "" private var lastLogin: String = ""
private var lastPassword: String = "" private var lastPassword: String = ""
private var errorTimer: Timer? = null private var errorTimer: Timer? = null
private var captchaInputLayout: TextInputLayout? = null private var captchaInputLayout: TextInputLayout? = null
private var validationInputLayout: TextInputLayout? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -60,11 +61,6 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
setFragmentResultListener("validation") { _, bundle -> setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(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) { override fun onEvent(event: VKEvent) {
@@ -72,9 +68,13 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
when (event) { when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription) is ShowError -> showErrorSnackbar(event.errorDescription)
is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid) is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
is GoToValidationEvent -> goToValidation(event.redirectUrl)
is GoToMainEvent -> goToMain(event.haveAuthorized) CodeSent -> showValidationDialog()
is ValidationRequired -> showValidationRequired()
is SuccessAuth -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted() StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressStopped() StopProgressEvent -> onProgressStopped()
} }
@@ -95,28 +95,11 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
} }
private fun prepareViews() { private fun prepareViews() {
prepareWebView()
prepareEmailEditText() prepareEmailEditText()
preparePasswordEditText() preparePasswordEditText()
prepareAuthButton() 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() { private fun prepareEmailEditText() {
binding.loginInput.addTextChangedListener { binding.loginInput.addTextChangedListener {
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = "" if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
@@ -150,31 +133,38 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
} }
private fun prepareAuthButton() { private fun prepareAuthButton() {
binding.auth.setOnClickListener { binding.auth.setOnClickListener { validateDataAndAuth() }
if (binding.progress.isVisible) return@setOnClickListener binding.auth.setOnLongClickListener {
validateDataAndAuth(BuildConfig.vkLogin to BuildConfig.vkPassword)
true
}
}
val loginString = binding.loginInput.text.toString().trim() private fun validateDataAndAuth(data: Pair<String, String>? = null) {
val passwordString = binding.passwordInput.text.toString().trim() 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
lastLogin = loginString
lastPassword = passwordString
KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
lifecycleScope.launch {
viewModel.login( viewModel.login(
binding.webView, login = loginString,
loginString, password = passwordString
passwordString
) )
} }
}
}
// TODO: 7/27/2021 extract strings to resources // TODO: 7/27/2021 extract strings to resources
private fun validateInputData( private fun validateInputData(
loginString: String?, loginString: String?,
passwordString: String?, passwordString: String?,
captchaCode: String? = null captchaCode: String? = null,
validationCode: String? = null
): Boolean { ): Boolean {
var isValidated = true var isValidated = true
@@ -193,6 +183,11 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
setError("Input code", captchaInputLayout!!) setError("Input code", captchaInputLayout!!)
} }
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
isValidated = false
setError("Input code", validationInputLayout!!)
}
return isValidated return isValidated
} }
@@ -220,7 +215,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
captchaInputLayout?.error = "" captchaInputLayout?.error = ""
} }
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) { private fun showCaptchaDialog(captchaSid: String, captchaImage: String) {
val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false) val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false)
captchaInputLayout = captchaBinding.captchaLayout captchaInputLayout = captchaBinding.captchaLayout
@@ -248,19 +243,52 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
dialog.dismiss() dialog.dismiss()
lifecycleScope.launch {
viewModel.login( viewModel.login(
webView = binding.webView, login = lastLogin,
email = lastEmail,
password = lastPassword, password = lastPassword,
captchaSid = captchaSid, captcha = captchaSid to captchaCode
captchaKey = captchaCode
) )
} }
}
captchaBinding.cancel.setOnClickListener { dialog.dismiss() } 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) { private fun showErrorSnackbar(errorDescription: String) {
val snackbar = Snackbar.make( val snackbar = Snackbar.make(
requireView(), requireView(),
@@ -272,19 +300,10 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
snackbar.show() snackbar.show()
} }
private fun goToValidation(redirectUrl: String) { private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
findNavController().navigate(
R.id.toValidation,
bundleOf("redirectUrl" to redirectUrl)
)
}
private fun goToMain(haveAuthorized: Boolean) {
lifecycleScope.launch {
if (haveAuthorized) delay(500) 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 import dagger.hilt.android.AndroidEntryPoint
@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() private val binding: FragmentMainBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 import kotlin.math.roundToInt
@AndroidEntryPoint @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 val binding: FragmentConversationsBinding by viewBinding()
private lateinit var adapter: ConversationsAdapter private lateinit var adapter: ConversationsAdapter
@@ -1,11 +1,11 @@
package com.meloda.fast.screens.messages package com.meloda.fast.screens.messages
import androidx.lifecycle.viewModelScope 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ConversationsVM : BaseVM() { class ConversationsViewModel : BaseViewModel() {
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { 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.api.model.*
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.api.OnResponseListener 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.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
@@ -11,7 +11,6 @@ import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.widget.CircleImageView import com.meloda.fast.widget.CircleImageView
import com.meloda.fast.api.model.VKUser import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil
object ViewUtils { object ViewUtils {
+9 -3
View File
@@ -12,7 +12,8 @@
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image" android:id="@+id/image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="100dp"
android:scaleType="fitCenter"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout <LinearLayout
@@ -65,7 +66,9 @@
android:layout_marginEnd="@dimen/activity_horizontal_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_weight="1" android:layout_weight="1"
android:text="@android:string/cancel" android:text="@android:string/cancel"
app:elevation="0dp" /> android:textColor="?colorAction"
app:elevation="0dp"
app:rippleColor="?colorActionRipple" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/ok" android:id="@+id/ok"
@@ -73,8 +76,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:backgroundTint="?colorAction"
android:text="@android:string/ok" android:text="@android:string/ok"
app:elevation="0dp" /> android:textColor="?colorActionContentPrimary"
app:elevation="0dp"
app:rippleColor="?colorActionRipple" />
</LinearLayout> </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:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
+2
View File
@@ -156,5 +156,7 @@
<string name="login_hint">Login</string> <string name="login_hint">Login</string>
<string name="conversations">Conversations</string> <string name="conversations">Conversations</string>
<string name="code_hint">Code</string>
<string name="input_validation_code">Input code from sms</string>
</resources> </resources>