Move from java/ to kotlin/ directory
Android 12 dynamic color usage on login screen
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package com.meloda.fast
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
|
||||
object UserConfig {
|
||||
|
||||
private const val TOKEN = "token"
|
||||
private const val USER_ID = "user_id"
|
||||
|
||||
const val API_ID = "6964679"
|
||||
|
||||
var userId: Int = -1
|
||||
get() = AppGlobal.preferences.getInt(USER_ID, -1)
|
||||
set(value) {
|
||||
field = value
|
||||
AppGlobal.preferences.edit().putInt(USER_ID, value).apply()
|
||||
}
|
||||
|
||||
var accessToken: String = ""
|
||||
get() = AppGlobal.preferences.getString(TOKEN, "") ?: ""
|
||||
set(value) {
|
||||
field = value
|
||||
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
accessToken = ""
|
||||
userId = -1
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean {
|
||||
return userId > 0 && !TextUtils.isEmpty(accessToken)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.meloda.fast
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
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 org.json.JSONArray
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
object VKLongPollParser {
|
||||
|
||||
|
||||
@WorkerThread
|
||||
fun parse(updates: JSONArray) {
|
||||
if (updates.length() == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (i in 0 until updates.length()) {
|
||||
val item = updates.optJSONArray(i)
|
||||
when (item.optInt(0)) {
|
||||
2 -> messageSetFlags(item)
|
||||
3 -> messageClearFlags(item)
|
||||
4 -> messageEvent(item)
|
||||
5 -> messageEdit(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseEvent(eventInfo: EventInfo<*>, onMessagesListener: OnMessagesListener) {
|
||||
when (eventInfo.key) {
|
||||
VKApiKeys.NEW_MESSAGE.value -> onMessagesListener.onNewMessage(eventInfo.data as VKMessage)
|
||||
VKApiKeys.EDIT_MESSAGE.value -> onMessagesListener.onEditMessage(eventInfo.data as VKMessage)
|
||||
VKApiKeys.RESTORE_MESSAGE.value -> onMessagesListener.onRestoredMessage(eventInfo.data as VKMessage)
|
||||
VKApiKeys.DELETE_MESSAGE.value -> {
|
||||
val array = eventInfo.data as Array<Int>
|
||||
onMessagesListener.onDeleteMessage(array[0], array[1])
|
||||
}
|
||||
VKApiKeys.READ_MESSAGE.value -> {
|
||||
val array = eventInfo.data as Array<Int>
|
||||
onMessagesListener.onReadMessage(array[0], array[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "VKLongPollParser"
|
||||
|
||||
private fun messageEvent(item: JSONArray) {
|
||||
val message = VKUtil.parseLongPollMessage(item)
|
||||
|
||||
TaskManager.execute {
|
||||
if (message.isFromUser()) {
|
||||
// VKUtil.searchUser(message.fromId)?.let { message.fromUser = it }
|
||||
} else {
|
||||
// VKUtil.searchGroup(message.fromId)?.let { message.fromGroup = it }
|
||||
}
|
||||
|
||||
// MemoryCache.getConversationById(message.peerId)?.let {
|
||||
// it.lastMessage = message
|
||||
// it.lastMessageId = message.messageId
|
||||
//
|
||||
// MemoryCache.put(it)
|
||||
// }
|
||||
//
|
||||
// MemoryCache.put(message)
|
||||
|
||||
val info = EventInfo(VKApiKeys.NEW_MESSAGE.name, message)
|
||||
|
||||
sendEvent(info)
|
||||
}
|
||||
}
|
||||
|
||||
private fun messageEdit(item: JSONArray) {
|
||||
val message = VKUtil.parseLongPollMessage(item)
|
||||
val info = EventInfo(VKApiKeys.EDIT_MESSAGE.name, message)
|
||||
|
||||
// MemoryCache.put(message)
|
||||
|
||||
sendEvent(info)
|
||||
}
|
||||
|
||||
private fun messageDelete(item: JSONArray) {
|
||||
val messageId = item.optInt(1)
|
||||
val peerId = item.optInt(3)
|
||||
val info = EventInfo(VKApiKeys.DELETE_MESSAGE.name, arrayOf(peerId, messageId))
|
||||
|
||||
// MemoryCache.deleteMessage(messageId)
|
||||
|
||||
sendEvent(info)
|
||||
}
|
||||
|
||||
private fun messageRestored(item: JSONArray) {
|
||||
val message = VKUtil.parseLongPollMessage(item)
|
||||
val info = EventInfo(VKApiKeys.RESTORE_MESSAGE.name, message)
|
||||
|
||||
// MemoryCache.put(message)
|
||||
|
||||
sendEvent(info)
|
||||
}
|
||||
|
||||
private fun messageRead(item: JSONArray) {
|
||||
val messageId = item.optInt(1)
|
||||
val peerId = item.optInt(3)
|
||||
val info = EventInfo(VKApiKeys.READ_MESSAGE.name, arrayOf(peerId, messageId))
|
||||
|
||||
// MemoryCache.edit(MemoryCache.getMessageById(messageId)?.apply { isRead = true })
|
||||
|
||||
sendEvent(info)
|
||||
}
|
||||
|
||||
private fun messageClearFlags(item: JSONArray) {
|
||||
val id = item.optInt(1)
|
||||
val flags = item.optInt(2)
|
||||
if (VKUtil.isMessageHasFlag(flags, "cancel_spam")) {
|
||||
Log.i(TAG, "Message with id $id: Not spam")
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "deleted")) {
|
||||
messageRestored(item)
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "important")) {
|
||||
Log.i(TAG, "Message with id $id: Not Important")
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "unread")) {
|
||||
messageRead(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun messageSetFlags(item: JSONArray) {
|
||||
val id = item.optInt(1)
|
||||
val flags = item.optInt(2)
|
||||
if (VKUtil.isMessageHasFlag(flags, "delete_for_all")) {
|
||||
messageDelete(item)
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "deleted")) {
|
||||
messageDelete(item)
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "spam")) {
|
||||
Log.i(TAG, "Message with id $id: Spam")
|
||||
}
|
||||
if (VKUtil.isMessageHasFlag(flags, "important")) {
|
||||
Log.i(TAG, "Message with id $id: Important")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(eventInfo: EventInfo<*>) {
|
||||
TaskManager.sendEvent(eventInfo)
|
||||
}
|
||||
|
||||
interface OnMessagesListener {
|
||||
fun onNewMessage(message: VKMessage)
|
||||
fun onEditMessage(message: VKMessage)
|
||||
fun onRestoredMessage(message: VKMessage)
|
||||
fun onDeleteMessage(peerId: Int, messageId: Int)
|
||||
fun onReadMessage(peerId: Int, messageId: Int)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.meloda.fast.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.viewbinding.library.activity.viewBinding
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.base.BaseActivity
|
||||
import com.meloda.fast.databinding.ActivityMainBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : BaseActivity(R.layout.activity_main) {
|
||||
|
||||
private val binding: ActivityMainBinding by viewBinding()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
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>()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
object ErrorCodes {
|
||||
const val UNKNOWN_ERROR = 1
|
||||
const val APP_DISABLED = 2
|
||||
const val UNKNOWN_METHOD = 3
|
||||
const val INVALID_SIGNATURE = 4
|
||||
const val USER_AUTHORIZATION_FAILED = 5
|
||||
const val TOO_MANY_REQUESTS = 6
|
||||
const val NO_RIGHTS = 7
|
||||
const val BAD_REQUEST = 8
|
||||
const val TOO_MANY_SIMILAR_ACTIONS = 9
|
||||
const val INTERNAL_SERVER_ERROR = 10
|
||||
const val IN_TEST_MODE = 11
|
||||
const val EXECUTE_CODE_COMPILE_ERROR = 12
|
||||
const val EXECUTE_CODE_RUNTIME_ERROR = 13
|
||||
const val CAPTCHA_NEEDED = 14
|
||||
const val ACCESS_DENIED = 15
|
||||
const val REQUIRES_REQUESTS_OVER_HTTPS = 16
|
||||
const val VALIDATION_REQUIRED = 17
|
||||
const val USER_BANNED_OR_DELETED = 18
|
||||
const val ACTION_PROHIBITED = 20
|
||||
const val ACTION_ALLOWED_ONLY_FOR_STANDALONE = 21
|
||||
const val METHOD_OFF = 23
|
||||
const val CONFIRMATION_REQUIRED = 24
|
||||
const val PARAMETER_IS_NOT_SPECIFIED = 100
|
||||
const val INCORRECT_APP_ID = 101
|
||||
const val OUT_OF_LIMITS = 103
|
||||
const val INCORRECT_USER_ID = 113
|
||||
const val INCORRECT_TIMESTAMP = 150
|
||||
const val ACCESS_TO_ALBUM_DENIED = 200
|
||||
const val ACCESS_TO_AUDIO_DENIED = 201
|
||||
const val ACCESS_TO_GROUP_DENIED = 203
|
||||
const val ALBUM_IS_FULL = 300
|
||||
const val ACTION_DENIED = 500
|
||||
const val PERMISSION_DENIED = 600
|
||||
const val CANNOT_SEND_MESSAGE_BLACK_LIST = 900
|
||||
const val CANNOT_SEND_MESSAGE_GROUP = 901
|
||||
const val INVALID_DOC_ID = 1150
|
||||
const val INVALID_DOC_TITLE = 1152
|
||||
const val ACCESS_TO_DOC_DENIED = 1153
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
interface OnResponseListener<T> {
|
||||
|
||||
fun onResponse(response: T)
|
||||
|
||||
fun onError(t: Throwable)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
object VKApi {
|
||||
|
||||
private const val TAG = "VKM:VKApi"
|
||||
|
||||
const val BASE_URL = "https://api.vk.com/method/"
|
||||
|
||||
const val API_VERSION = "5.132"
|
||||
|
||||
var language: String = ""
|
||||
var token: String = ""
|
||||
|
||||
private lateinit var handler: Handler
|
||||
|
||||
fun init(language: String, token: String, handler: Handler) {
|
||||
VKApi.language = language
|
||||
VKApi.token = token
|
||||
VKApi.handler = handler
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> execute(url: String, cls: Class<T>?): ArrayList<T>? {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "url: $url")
|
||||
}
|
||||
|
||||
val buffer = com.meloda.fast.net.HttpRequest[url].asString()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(TAG, "response: $buffer")
|
||||
}
|
||||
|
||||
val json = JSONObject(buffer)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
when (cls) {
|
||||
null -> return null
|
||||
|
||||
com.meloda.fast.api.model.VKLongPollServer::class.java -> {
|
||||
json.optJSONObject("response")?.let {
|
||||
return arrayListOf(com.meloda.fast.api.model.VKLongPollServer(it)) as ArrayList<T>?
|
||||
}
|
||||
}
|
||||
|
||||
Boolean::class.java -> {
|
||||
val value = json.optInt("response") == 1
|
||||
return arrayListOf(value) as ArrayList<T>?
|
||||
}
|
||||
|
||||
Long::class.java -> {
|
||||
val value = json.optLong("response")
|
||||
return arrayListOf(value) as ArrayList<T>?
|
||||
}
|
||||
|
||||
Int::class.java -> {
|
||||
val value = json.optInt("response")
|
||||
return arrayListOf(value) as ArrayList<T>?
|
||||
}
|
||||
}
|
||||
|
||||
val response = json.opt("response") ?: return null
|
||||
|
||||
val array = optItems(json) ?: return null
|
||||
val models = ArrayList<T>(array.length())
|
||||
|
||||
when (cls) {
|
||||
com.meloda.fast.api.model.VKUser::class.java -> {
|
||||
json.optJSONObject("response")?.let { r ->
|
||||
com.meloda.fast.api.model.VKUser.friendsCount = r.optInt("count")
|
||||
}
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
models.add(com.meloda.fast.api.model.VKUser(array.optJSONObject(i)) as T)
|
||||
}
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKMessage::class.java -> {
|
||||
response as JSONObject
|
||||
|
||||
if (url.contains("messages.getHistory")) {
|
||||
com.meloda.fast.api.model.VKMessage.lastHistoryCount = response.optInt("count")
|
||||
|
||||
response.optJSONArray("profiles")?.let {
|
||||
val profiles = arrayListOf<com.meloda.fast.api.model.VKUser>()
|
||||
|
||||
for (j in 0 until it.length()) {
|
||||
profiles.add(com.meloda.fast.api.model.VKUser(it.optJSONObject(j)))
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKMessage.profiles = profiles
|
||||
}
|
||||
|
||||
response.optJSONArray("groups")?.let {
|
||||
val groups = arrayListOf<com.meloda.fast.api.model.VKGroup>()
|
||||
|
||||
for (j in 0 until it.length()) {
|
||||
groups.add(com.meloda.fast.api.model.VKGroup(it.optJSONObject(j)))
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKMessage.groups = groups
|
||||
}
|
||||
|
||||
response.optJSONArray("conversations")?.let {
|
||||
val conversations = arrayListOf<com.meloda.fast.api.model.VKConversation>()
|
||||
|
||||
for (j in 0 until it.length()) {
|
||||
conversations.add(
|
||||
com.meloda.fast.api.model.VKConversation(
|
||||
it.optJSONObject(
|
||||
j
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKMessage.conversations = conversations
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
var source = array.optJSONObject(i)
|
||||
if (source.has("message")) {
|
||||
source = source.optJSONObject("message")
|
||||
}
|
||||
|
||||
val message = com.meloda.fast.api.model.VKMessage(source)
|
||||
models.add(message as T)
|
||||
}
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKGroup::class.java -> {
|
||||
for (i in 0 until array.length()) {
|
||||
models.add(com.meloda.fast.api.model.VKGroup(array.optJSONObject(i)) as T)
|
||||
}
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKModel::class.java -> {
|
||||
if (url.contains("messages.getHistoryAttachments")) {
|
||||
return com.meloda.fast.api.model.VKAttachments.parse(array) as ArrayList<T>
|
||||
}
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKConversation::class.java -> {
|
||||
if (url.contains("getConversationsById")) {
|
||||
for (i in 0 until array.length()) {
|
||||
val source = array.optJSONObject(i)
|
||||
models.add(com.meloda.fast.api.model.VKConversation(source) as T)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
json.optJSONObject("response")?.let { r ->
|
||||
com.meloda.fast.api.model.VKConversation.conversationsCount = r.optInt("count")
|
||||
}
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
response as JSONObject
|
||||
|
||||
val source = array.optJSONObject(i)
|
||||
val oConversation = source.optJSONObject("conversation") ?: return null
|
||||
val oLastMessage = source.optJSONObject("last_message") ?: return null
|
||||
|
||||
val conversation = com.meloda.fast.api.model.VKConversation(oConversation).also {
|
||||
it.lastMessage = com.meloda.fast.api.model.VKMessage(oLastMessage)
|
||||
}
|
||||
|
||||
response.optJSONArray("profiles")?.let {
|
||||
val profiles = arrayListOf<com.meloda.fast.api.model.VKUser>()
|
||||
|
||||
for (j in 0 until it.length()) {
|
||||
profiles.add(com.meloda.fast.api.model.VKUser(it.optJSONObject(j)))
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKConversation.profiles = profiles
|
||||
}
|
||||
|
||||
response.optJSONArray("groups")?.let {
|
||||
val groups = arrayListOf<com.meloda.fast.api.model.VKGroup>()
|
||||
|
||||
for (j in 0 until it.length()) {
|
||||
groups.add(com.meloda.fast.api.model.VKGroup(it.optJSONObject(j)))
|
||||
}
|
||||
|
||||
com.meloda.fast.api.model.VKConversation.groups = groups
|
||||
}
|
||||
|
||||
models.add(conversation as T)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
fun <E> execute(url: String, cls: Class<E>, listener: OnResponseListener<E>?) {
|
||||
com.meloda.fast.concurrent.TaskManager.execute {
|
||||
try {
|
||||
val models = execute(url, cls) ?: return@execute
|
||||
|
||||
// listener?.onResponse(models)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
listener?.onError(e)
|
||||
// it.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun <E> suspendExecute(url: String, cls: Class<E>): Flow<E> {
|
||||
return suspendCoroutine {
|
||||
try {
|
||||
val models = execute(url, cls)?.asFlow() ?: return@suspendCoroutine
|
||||
|
||||
it.resume(models)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
it.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <E> executeArray(url: String, cls: Class<E>, listener: OnResponseListener<ArrayList<E>>) {
|
||||
com.meloda.fast.concurrent.TaskManager.execute {
|
||||
try {
|
||||
val models = execute(url, cls)
|
||||
|
||||
handler.post { listener.onResponse(models as ArrayList<E>) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
listener.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun optItems(source: JSONObject): JSONArray? {
|
||||
val response = source.opt("response")
|
||||
|
||||
return when (response) {
|
||||
is JSONArray -> response
|
||||
is JSONObject -> response.optJSONArray("items")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkError(json: JSONObject, url: String) {
|
||||
if (json.has("error")) {
|
||||
val error = json.optJSONObject("error") ?: return
|
||||
|
||||
val code = error.optInt("error_code", -1)
|
||||
val message = error.optString("error_msg", "")
|
||||
val e = VKException(url, 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.optString("captcha_img")
|
||||
e.captchaSid = error.optString("captcha_sid")
|
||||
}
|
||||
|
||||
if (code == ErrorCodes.VALIDATION_REQUIRED) {
|
||||
e.redirectUri = error.optString("redirect_uri")
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun users(): VKUsers {
|
||||
return VKUsers()
|
||||
}
|
||||
|
||||
fun friends(): VKFriends {
|
||||
return VKFriends()
|
||||
}
|
||||
|
||||
fun messages(): VKMessages {
|
||||
return VKMessages()
|
||||
}
|
||||
|
||||
fun groups(): VKGroups {
|
||||
return VKGroups()
|
||||
}
|
||||
|
||||
fun account(): VKAccounts {
|
||||
return VKAccounts()
|
||||
}
|
||||
|
||||
class VKFriends {
|
||||
fun get(): MethodSetter {
|
||||
return MethodSetter("friends.get")
|
||||
}
|
||||
}
|
||||
|
||||
class VKUsers {
|
||||
fun get(): UserMethodSetter {
|
||||
return UserMethodSetter("users.get")
|
||||
}
|
||||
}
|
||||
|
||||
class VKMessages {
|
||||
fun get(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.get")
|
||||
}
|
||||
|
||||
fun getConversations(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getConversations")
|
||||
}
|
||||
|
||||
fun getConversationsById(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getConversationsById")
|
||||
}
|
||||
|
||||
fun getById(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getById")
|
||||
}
|
||||
|
||||
fun search(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.search")
|
||||
}
|
||||
|
||||
fun getHistory(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getHistory")
|
||||
}
|
||||
|
||||
fun getHistoryAttachments(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getHistoryAttachments")
|
||||
}
|
||||
|
||||
fun send(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.send")
|
||||
}
|
||||
|
||||
fun sendSticker(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.sendSticker")
|
||||
}
|
||||
|
||||
fun delete(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.delete")
|
||||
}
|
||||
|
||||
fun deleteDialog(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.deleteDialog")
|
||||
}
|
||||
|
||||
fun restore(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.restore")
|
||||
}
|
||||
|
||||
fun markAsRead(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.markAsRead")
|
||||
}
|
||||
|
||||
fun markAsImportant(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.markAsImportant")
|
||||
}
|
||||
|
||||
fun getLongPollServer(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getLongPollServer")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns updates in user's private messages.
|
||||
* To speed up handling of private messages,
|
||||
* it can be useful to cache previously loaded messages on
|
||||
* a user's mobile device/desktop, to prevent re-receipt at each call.
|
||||
* With this method, you can synchronize a local copy of
|
||||
* the message list with the actual version.
|
||||
*
|
||||
*
|
||||
* Result:
|
||||
* Returns an object that contains the following fields:
|
||||
* 1 — history: An array similar to updates field returned
|
||||
* from the Long Poll server,
|
||||
* with these exceptions:
|
||||
* - For events with code 4 (addition of a new message),
|
||||
* there are no fields except the first three.
|
||||
* - There are no events with codes 8, 9 (friend goes online/offline)
|
||||
* or with codes 61, 62 (typing during conversation/chat).
|
||||
*
|
||||
*
|
||||
* 2 — messages: An array of private message objects that were found
|
||||
* among events with code 4 (addition of a new message)
|
||||
* from the history field.
|
||||
* Each object of message contains a set of fields described here.
|
||||
* The first array element is the total number of messages
|
||||
*/
|
||||
fun getLongPollHistory(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getLongPollHistory")
|
||||
}
|
||||
|
||||
fun getChat(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.getChat")
|
||||
}
|
||||
|
||||
fun createChat(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.createChat")
|
||||
}
|
||||
|
||||
fun editChat(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.editChat")
|
||||
}
|
||||
|
||||
val chatUsers: MessageMethodSetter
|
||||
get() = MessageMethodSetter("messages.getChatUsers")
|
||||
|
||||
fun setActivity(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.setActivity").type(true)
|
||||
}
|
||||
|
||||
fun addChatUser(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.addChatUser")
|
||||
}
|
||||
|
||||
fun removeChatUser(): MessageMethodSetter {
|
||||
return MessageMethodSetter("messages.removeChatUser")
|
||||
}
|
||||
}
|
||||
|
||||
class VKGroups {
|
||||
fun getById(): MethodSetter {
|
||||
return MethodSetter("groups.getById")
|
||||
}
|
||||
|
||||
fun join(): MethodSetter {
|
||||
return MethodSetter("groups.join")
|
||||
}
|
||||
}
|
||||
|
||||
class VKAccounts {
|
||||
fun setOffline(): MethodSetter {
|
||||
return MethodSetter("account.setOffline")
|
||||
}
|
||||
|
||||
fun setOnline(): MethodSetter {
|
||||
return MethodSetter("account.setOnline")
|
||||
}
|
||||
}
|
||||
|
||||
class SuccessCallback<E>(
|
||||
private val listener: OnResponseListener<E>?,
|
||||
private val response: E
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
listener?.onResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
class SuccessArrayCallback<E>(
|
||||
private val listener: OnResponseListener<ArrayList<E>>?,
|
||||
private val response: ArrayList<E>
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
listener?.onResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorCallback<E>(
|
||||
private val listener: OnResponseListener<E>?,
|
||||
private val exception: Exception
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
listener?.onError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
enum class VKApiKeys(val value: String) {
|
||||
READ_MESSAGE("_read_message"),
|
||||
RESTORE_MESSAGE("_restore_message"),
|
||||
NEW_MESSAGE("_new_message"),
|
||||
EDIT_MESSAGE("_edit_message"),
|
||||
DELETE_MESSAGE("_delete_message"),
|
||||
|
||||
UPDATE_MESSAGE("_update_message"),
|
||||
UPDATE_CONVERSATION("_update_conversation"),
|
||||
UPDATE_USER("_update_user"),
|
||||
UPDATE_GROUP("_update_group")
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import android.util.Log
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.api.util.VKUtil
|
||||
import java.net.URLEncoder
|
||||
|
||||
object VKAuth {
|
||||
|
||||
private const val TAG = "VKM.VKAuth"
|
||||
|
||||
const val settings =
|
||||
"notify," +
|
||||
"friends," +
|
||||
"photos," +
|
||||
"audio," +
|
||||
"video," +
|
||||
"docs," +
|
||||
"status," +
|
||||
"notes," +
|
||||
"pages," +
|
||||
"wall," +
|
||||
"groups," +
|
||||
"messages," +
|
||||
"offline," +
|
||||
"notifications"
|
||||
|
||||
const val redirectUrl = "https://oauth.vk.com/blank.html"
|
||||
|
||||
fun getDirectAuthUrl(
|
||||
login: String,
|
||||
password: String,
|
||||
captchaSid: String? = null,
|
||||
captchaKey: String? = null
|
||||
): String {
|
||||
return "https://oauth.vk.com/token?grant_type=password&" +
|
||||
"client_id=${VKConstants.VK_APP_ID}&" +
|
||||
"scope=$settings&" +
|
||||
"client_secret=${VKConstants.VK_APP_SECRET}&" +
|
||||
"username=$login&" +
|
||||
"password=$password" +
|
||||
(if (captchaSid == null || captchaKey == null) "" else "&captcha_sid=$captchaSid&captcha_key=$captchaKey") +
|
||||
"&v=${VKApi.API_VERSION}"
|
||||
// return "https://oauth.vk.com/token?grant_type=password&" +
|
||||
// "client_id=2274003&" +
|
||||
// "scope=notify,friends,photos,audio,video,docs,notes,pages,status,offers,questions,wall,groups,messages,email,notifications,stats,ads,market,offline&" +
|
||||
// "client_secret=hHbZxrka2uZ6jB1inYsH&" +
|
||||
// "username=$login&" +
|
||||
// "password=$password" +
|
||||
// (if (captcha.isEmpty()) "" else "&$captcha") +
|
||||
// "&v=${VKApi.API_VERSION}"
|
||||
}
|
||||
|
||||
fun getUrl(api_id: String, settings: String): String {
|
||||
return "https://oauth.vk.com/authorize?" +
|
||||
"client_id=$api_id&" +
|
||||
"display=mobile&" +
|
||||
"scope=$settings&" +
|
||||
"redirect_uri=${
|
||||
URLEncoder.encode(
|
||||
redirectUrl,
|
||||
"utf-8"
|
||||
)
|
||||
}&" +
|
||||
"response_type=token&" +
|
||||
"v=${URLEncoder.encode(VKApi.API_VERSION, "utf-8")}"
|
||||
}
|
||||
|
||||
fun parseRedirectUrl(url: String): Pair<String, Int> {
|
||||
val accessToken = VKUtil.extractPattern(url, "access_token=(.*?)&") ?: ""
|
||||
val userId = VKUtil.extractPattern(url, "user_id=(\\d*)")?.toIntOrNull() ?: -1
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(TAG, "access_token=$accessToken")
|
||||
Log.i(TAG, "user_id=$userId")
|
||||
}
|
||||
|
||||
if (accessToken.isEmpty() || userId == -1) throw Exception(
|
||||
"Failed to parse redirect url: $url"
|
||||
)
|
||||
|
||||
return accessToken to userId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
object VKConstants {
|
||||
|
||||
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
|
||||
const val USER_FIELDS =
|
||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
|
||||
|
||||
const val VK_APP_ID = "2274003"
|
||||
const val VK_APP_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
|
||||
const val VK_ME_ID = "6146827"
|
||||
const val VK_ME_SECRET = "qVxWRF1CwHERuIrKBnqe"
|
||||
|
||||
/*
|
||||
|
||||
const val ACTION_CHAT_CREATE = "chat_create"
|
||||
const val ACTION_PHOTO_UPDATE = "chat_photo_update"
|
||||
const val ACTION_PHOTO_REMOVE = "chat_photo_remove"
|
||||
const val ACTION_TITLE_UPDATE = "chat_title_update"
|
||||
const val ACTION_PIN_MESSAGE = "chat_pin_message"
|
||||
const val ACTION_UNPIN_MESSAGE = "chat_unpin_message"
|
||||
const val ACTION_INVITE_USER = "chat_invite_user"
|
||||
const val ACTION_INVITE_USER_BY_LINK = "chat_invite_user_by_link"
|
||||
const val ACTION_KICK_USER = "chat_kick_user"
|
||||
const val ACTION_SCREENSHOT = "chat_screenshot"
|
||||
const val ACTION_INVITE_USER_BY_CALL = "chat_invite_user_by_call"
|
||||
const val ACTION_INVITE_USER_BY_CALL_JOIN_LINK = "chat_invite_user_by_call_link"
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
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
|
||||
|
||||
override fun toString(): String {
|
||||
return "code: $code, message: $message"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
object VKUrls {
|
||||
|
||||
const val getConversations = "messages.getConversations"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.meloda.fast.api.datasource
|
||||
|
||||
class MessagesDataSource constructor() {
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.meloda.fast.api.method
|
||||
|
||||
import com.meloda.fast.util.ArrayUtils
|
||||
|
||||
class MessageMethodSetter(name: String) : MethodSetter(name) {
|
||||
|
||||
fun out(value: Boolean): MessageMethodSetter {
|
||||
put("out", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun timeOffset(value: Int): MessageMethodSetter {
|
||||
put("time_offset", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun filters(value: Int): MessageMethodSetter {
|
||||
put("filters", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun previewLength(value: Int): MessageMethodSetter {
|
||||
put("preview_length", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun lastMessageId(value: Int): MessageMethodSetter {
|
||||
put("last_message_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun unread(value: Boolean): MessageMethodSetter {
|
||||
put("unread", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun messageIds(vararg ids: Int): MessageMethodSetter {
|
||||
put("message_ids", ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun messageIds(ids: ArrayList<Int>): MessageMethodSetter {
|
||||
put("message_ids", ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun q(query: String): MessageMethodSetter {
|
||||
put("q", query)
|
||||
return this
|
||||
}
|
||||
|
||||
fun startMessageId(id: Int): MessageMethodSetter {
|
||||
put("start_message_id", id)
|
||||
return this
|
||||
}
|
||||
|
||||
fun peerId(value: Int): MessageMethodSetter {
|
||||
put("peer_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun peerIds(vararg values: Int): MessageMethodSetter {
|
||||
put("peer_ids", com.meloda.fast.util.ArrayUtils.asString(values))
|
||||
return this
|
||||
}
|
||||
|
||||
fun reversed(value: Boolean): MessageMethodSetter {
|
||||
put("rev", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun domain(value: String): MessageMethodSetter {
|
||||
put("domain", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun chatId(value: Int): MessageMethodSetter {
|
||||
put("chat_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun message(message: String): MessageMethodSetter {
|
||||
put("message", message)
|
||||
return this
|
||||
}
|
||||
|
||||
fun randomId(value: Int): MessageMethodSetter {
|
||||
put("random_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun lat(lat: Double): MessageMethodSetter {
|
||||
put("lat", lat)
|
||||
return this
|
||||
}
|
||||
|
||||
fun longitude(value: Long): MessageMethodSetter {
|
||||
put("LONG", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun attachment(attachments: Collection<String>): MessageMethodSetter {
|
||||
put("attachment", com.meloda.fast.util.ArrayUtils.asString(attachments))
|
||||
return this
|
||||
}
|
||||
|
||||
fun attachment(vararg attachments: String): MessageMethodSetter {
|
||||
put("attachment", com.meloda.fast.util.ArrayUtils.asString(*attachments))
|
||||
return this
|
||||
}
|
||||
|
||||
fun forwardMessages(ids: Collection<String>): MessageMethodSetter {
|
||||
put("forward_messages", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun forwardMessages(vararg ids: Int): MessageMethodSetter {
|
||||
put("forward_messages", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun stickerId(value: Int): MessageMethodSetter {
|
||||
put("sticker_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun messageId(value: Int): MessageMethodSetter {
|
||||
put("message_id", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun important(value: Boolean): MessageMethodSetter {
|
||||
put("important", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun ts(value: Long): MessageMethodSetter {
|
||||
put("ts", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun pts(value: Int): MessageMethodSetter {
|
||||
put("pts", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun msgsLimit(limit: Int): MessageMethodSetter {
|
||||
put("msgs_limit", limit)
|
||||
return this
|
||||
}
|
||||
|
||||
fun onlines(onlines: Boolean): MessageMethodSetter {
|
||||
put("onlines", onlines)
|
||||
return this
|
||||
}
|
||||
|
||||
fun maxMsgId(id: Int): MessageMethodSetter {
|
||||
put("max_msg_id", id)
|
||||
return this
|
||||
}
|
||||
|
||||
fun chatIds(vararg ids: Int): MessageMethodSetter {
|
||||
put("max_msg_id", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun chatIds(ids: Collection<Int>): MessageMethodSetter {
|
||||
put("max_msg_id", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
return this
|
||||
}
|
||||
|
||||
fun title(title: String): MessageMethodSetter {
|
||||
put("title", title)
|
||||
return this
|
||||
}
|
||||
|
||||
fun type(typing: Boolean): MessageMethodSetter {
|
||||
if (typing) {
|
||||
put("type", "typing")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun mediaType(type: String): MessageMethodSetter {
|
||||
put("media_type", type)
|
||||
return this
|
||||
}
|
||||
|
||||
fun photoSizes(value: Boolean): MessageMethodSetter {
|
||||
return put("photo_sizes", value) as MessageMethodSetter
|
||||
}
|
||||
|
||||
fun filter(value: String): MessageMethodSetter {
|
||||
return put("filter", value) as MessageMethodSetter
|
||||
}
|
||||
|
||||
fun extended(value: Boolean): MessageMethodSetter {
|
||||
return put("extended", value) as MessageMethodSetter
|
||||
}
|
||||
|
||||
fun markConversationAsRead(asRead: Boolean): MessageMethodSetter {
|
||||
put("mark_conversation_as_read", asRead)
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.meloda.fast.api.method
|
||||
|
||||
import android.util.ArrayMap
|
||||
import android.util.Log
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.api.OnResponseListener
|
||||
import com.meloda.fast.api.VKApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class MethodSetter(private val name: String) {
|
||||
|
||||
private val params: ArrayMap<String, String> = ArrayMap()
|
||||
|
||||
fun put(key: String, value: Any): MethodSetter {
|
||||
params[key] = value.toString()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(key: String, value: String): MethodSetter {
|
||||
params[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(key: String, value: Int): MethodSetter {
|
||||
params[key] = value.toString()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(key: String, value: Long): MethodSetter {
|
||||
params[key] = value.toString()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(key: String, value: Boolean): MethodSetter {
|
||||
params[key] = if (value) "1" else "0"
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getSignedUrl(): String {
|
||||
if (!params.containsKey("access_token")) {
|
||||
params["access_token"] = VKApi.token
|
||||
}
|
||||
|
||||
if (!params.containsKey("v")) {
|
||||
params["v"] = VKApi.API_VERSION
|
||||
}
|
||||
|
||||
if (!params.containsKey("lang")) {
|
||||
params["lang"] = VKApi.language
|
||||
}
|
||||
|
||||
return "${VKApi.BASE_URL}$name?${retrieveParams()}"
|
||||
}
|
||||
|
||||
private fun retrieveParams(): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
for (i in 0 until params.size) {
|
||||
val key = params.keyAt(i)
|
||||
val value = params.valueAt(i)
|
||||
|
||||
if (builder.isNotEmpty()) {
|
||||
builder.append("&")
|
||||
}
|
||||
|
||||
builder.append(key)
|
||||
builder.append("=")
|
||||
builder.append(URLEncoder.encode(value, "UTF-8"))
|
||||
}
|
||||
|
||||
val params = builder.toString()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i("MethodSetter", "retrieved params: $params")
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
suspend fun <E> executeSuspend(cls: Class<E>): Flow<E> {
|
||||
return VKApi.suspendExecute(getSignedUrl(), cls)
|
||||
}
|
||||
|
||||
fun <E> execute(cls: Class<E>): ArrayList<E>? {
|
||||
return VKApi.execute(getSignedUrl(), cls)
|
||||
}
|
||||
|
||||
fun <E> executeArray(cls: Class<E>, listener: OnResponseListener<ArrayList<E>>) {
|
||||
VKApi.executeArray(getSignedUrl(), cls, listener)
|
||||
}
|
||||
|
||||
fun <E> execute(cls: Class<E>, listener: OnResponseListener<E>?) {
|
||||
VKApi.execute(getSignedUrl(), cls, listener)
|
||||
}
|
||||
|
||||
fun userId(value: Int): MethodSetter {
|
||||
return put("user_id", value)
|
||||
}
|
||||
|
||||
fun userIds(vararg ids: Int): MethodSetter {
|
||||
return put("user_ids", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
}
|
||||
|
||||
fun userIds(ids: ArrayList<Int>): MethodSetter {
|
||||
return put("user_ids", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
}
|
||||
|
||||
fun ownerId(value: Int): MethodSetter {
|
||||
return put("owner_id", value)
|
||||
}
|
||||
|
||||
fun groupId(value: Int): MethodSetter {
|
||||
return put("group_id", value)
|
||||
}
|
||||
|
||||
fun groupIds(vararg ids: Int): MethodSetter {
|
||||
return put("group_ids", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
}
|
||||
|
||||
fun groupIds(ids: ArrayList<Int>): MethodSetter {
|
||||
return put("group_ids", com.meloda.fast.util.ArrayUtils.asString(ids))
|
||||
}
|
||||
|
||||
fun fields(values: String): MethodSetter {
|
||||
return put("fields", values)
|
||||
}
|
||||
|
||||
fun count(value: Int): MethodSetter {
|
||||
return put("count", value)
|
||||
}
|
||||
|
||||
fun sort(value: Int): MethodSetter {
|
||||
put("sort", value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* hints — сортировать по рейтингу, аналогично тому, как друзья сортируются в разделе Мои друзья
|
||||
* random — возвращает друзей в случайном порядке.
|
||||
* mobile — возвращает выше тех друзей, у которых установлены мобильные приложения.
|
||||
* name — сортировать по имени (долго)
|
||||
*
|
||||
*/
|
||||
|
||||
fun order(value: String): MethodSetter {
|
||||
put("order", value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun offset(value: Int = 0): MethodSetter {
|
||||
return put("offset", value)
|
||||
}
|
||||
|
||||
fun nameCase(value: String): MethodSetter {
|
||||
return put("name_case", value)
|
||||
}
|
||||
|
||||
fun captchaSid(value: String): MethodSetter {
|
||||
return put("captcha_sid", value)
|
||||
}
|
||||
|
||||
fun captchaKey(value: String): MethodSetter {
|
||||
return put("captcha_key", value)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.meloda.fast.api.method
|
||||
|
||||
class UserMethodSetter(name: String) : MethodSetter(name) {
|
||||
|
||||
fun extended(extended: Boolean): UserMethodSetter {
|
||||
put("extended", extended)
|
||||
return this
|
||||
}
|
||||
|
||||
fun type(type: String): UserMethodSetter {
|
||||
put("type", type)
|
||||
return this
|
||||
}
|
||||
|
||||
fun comment(comment: String): UserMethodSetter {
|
||||
put("comment", comment)
|
||||
return this
|
||||
}
|
||||
|
||||
fun latitude(latitude: Float): UserMethodSetter {
|
||||
put("latitude", latitude)
|
||||
return this
|
||||
}
|
||||
|
||||
fun longitude(longitude: Float): UserMethodSetter {
|
||||
put("longitude", longitude)
|
||||
return this
|
||||
}
|
||||
|
||||
fun accuracy(accuracy: Int): UserMethodSetter {
|
||||
put("accuracy", accuracy)
|
||||
return this
|
||||
}
|
||||
|
||||
fun timeout(timeout: Int): UserMethodSetter {
|
||||
put("timeout", timeout)
|
||||
return this
|
||||
}
|
||||
|
||||
fun radius(radius: Int): UserMethodSetter {
|
||||
put("radius", radius)
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONArray
|
||||
import java.util.*
|
||||
|
||||
object VKAttachments {
|
||||
|
||||
fun parse(array: JSONArray): ArrayList<VKModel> {
|
||||
val attachments = ArrayList<VKModel>(array.length())
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
var attachment = array.optJSONObject(i) ?: continue
|
||||
if (attachment.has("attachment")) {
|
||||
attachment = attachment.optJSONObject("attachment") ?: continue
|
||||
}
|
||||
|
||||
val type = Type.fromString(attachment.optString("type"))
|
||||
val jsonObject = attachment.optJSONObject(type.value) ?: continue
|
||||
|
||||
when (type) {
|
||||
Type.PHOTO -> attachments.add(VKPhoto(jsonObject))
|
||||
Type.AUDIO -> attachments.add(VKAudio(jsonObject))
|
||||
Type.VIDEO -> attachments.add(VKVideo(jsonObject))
|
||||
Type.DOCUMENT -> attachments.add(VKDocument(jsonObject))
|
||||
Type.STICKER -> attachments.add(VKSticker(jsonObject))
|
||||
Type.LINK -> attachments.add(VKLink(jsonObject))
|
||||
Type.GIFT -> attachments.add(VKGift(jsonObject))
|
||||
Type.VOICE_MESSAGE -> attachments.add(VKAudioMessage(jsonObject))
|
||||
Type.GRAFFITI -> attachments.add(VKGraffiti(jsonObject))
|
||||
Type.POLL -> attachments.add(VKPoll(jsonObject))
|
||||
Type.CALL -> attachments.add(VKCall(jsonObject))
|
||||
Type.WALL_POST -> attachments.add(VKWall(jsonObject))
|
||||
Type.WALL_REPLY -> attachments.add(VKComment(jsonObject))
|
||||
Type.GEOLOCATION -> attachments.add(VKGeolocation(jsonObject))
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
enum class Type(val value: String) {
|
||||
NONE("none"),
|
||||
PHOTO("photo"),
|
||||
VIDEO("video"),
|
||||
AUDIO("audio"),
|
||||
AUDIO_PLAYLIST("audio_playlist"),
|
||||
DOCUMENT("doc"),
|
||||
LINK("link"),
|
||||
STICKER("sticker"),
|
||||
GIFT("gift"),
|
||||
VOICE_MESSAGE("audio_message"),
|
||||
GRAFFITI("graffiti"),
|
||||
POLL("poll"),
|
||||
GEOLOCATION("geo"),
|
||||
WALL_POST("wall"),
|
||||
WALL_REPLY("wall_reply"),
|
||||
CALL("call"),
|
||||
STORY("story"),
|
||||
POINT("point"),
|
||||
MARKET("market"),
|
||||
ARTICLE("article"),
|
||||
PODCAST("podcast"),
|
||||
MONEY_REQUEST("money_request");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): Type {
|
||||
for (v in values()) {
|
||||
if (v.value == value) return v
|
||||
}
|
||||
|
||||
return NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKAudio() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.AUDIO
|
||||
|
||||
var id: Int = 0
|
||||
var ownerId: Int = 0
|
||||
var artist: String = ""
|
||||
var title: String = ""
|
||||
var duration: Int = 0
|
||||
var url: String = ""
|
||||
var date: Int = 0
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
ownerId = o.optInt("owner_id", -1)
|
||||
artist = o.optString("artist")
|
||||
title = o.optString("title")
|
||||
duration = o.optInt("duration")
|
||||
url = o.optString("url")
|
||||
date = o.optInt("date")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKAudioMessage() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.VOICE_MESSAGE
|
||||
|
||||
var duration: Int = 0
|
||||
var waveform: ArrayList<Int> = arrayListOf()
|
||||
var linkOgg: String = ""
|
||||
var linkMp3: String = ""
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
duration = o.optInt("duration")
|
||||
linkOgg = o.optString("link_ogg")
|
||||
linkMp3 = o.optString("link_mp3")
|
||||
|
||||
o.optJSONArray("waveform")?.let {
|
||||
val waveform = ArrayList<Int>()
|
||||
for (i in 0 until it.length()) {
|
||||
waveform.add(it.optInt(i))
|
||||
}
|
||||
this.waveform = waveform
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKCall() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.CALL
|
||||
|
||||
var initiatorId: Int = 0
|
||||
var receiverId: Int = 0
|
||||
var state: State = State.NONE
|
||||
var time: Int = 0
|
||||
var duration: Int = 0
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
initiatorId = o.optInt("initiator_id", -1)
|
||||
receiverId = o.optInt("receiver_id", -1)
|
||||
state = State.fromString(o.optString("state"))
|
||||
time = o.optInt("time")
|
||||
duration = o.optInt("duration")
|
||||
}
|
||||
|
||||
enum class State(val value: String) {
|
||||
NONE("none"),
|
||||
REACHED("reached"),
|
||||
CANCELLED_INITIATOR("canceled_by_initiator"),
|
||||
CANCELLED_RECEIVER("canceled_by_receiver");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKComment() : VKModel() { //https://vk.com/dev/objects/comment
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.WALL_REPLY
|
||||
|
||||
constructor(o: JSONObject) : this() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKConversation() : VKModel(), Cloneable {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
|
||||
var profiles = arrayListOf<VKUser>()
|
||||
var groups = arrayListOf<VKGroup>()
|
||||
|
||||
var conversationsCount: Int = 0
|
||||
|
||||
var count: Int = 0
|
||||
}
|
||||
|
||||
var isAllowed: Boolean = false
|
||||
var notAllowedReason: Reason = Reason.NULL
|
||||
|
||||
var inReadMessageId: Int = 0
|
||||
var outReadMessageId: Int = 0
|
||||
var lastMessageId: Int = 0
|
||||
var unreadCount: Int = 0
|
||||
|
||||
var id: Int = 0
|
||||
|
||||
var intType: Int = 0
|
||||
var type: Type = Type.NULL
|
||||
|
||||
var localId: Int = 0
|
||||
|
||||
var notificationsEnabled: Boolean = false
|
||||
|
||||
var disabledUntil: Int = 0
|
||||
var isDisabledForever: Boolean = false
|
||||
var isNoSound: Boolean = false
|
||||
|
||||
var membersCount: Int = 0
|
||||
var title: String? = null
|
||||
|
||||
var pinnedMessage: VKMessage? = null
|
||||
|
||||
var intState: Int = 0
|
||||
var state: State = State.IN
|
||||
|
||||
var lastMessage: VKMessage = VKMessage()
|
||||
|
||||
var isGroupChannel: Boolean = false
|
||||
|
||||
var photo50: String = ""
|
||||
var photo100: String = ""
|
||||
var photo200: String = ""
|
||||
|
||||
var peerUser: VKUser? = null
|
||||
|
||||
var peerGroup: VKGroup? = null
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
inReadMessageId = o.optInt("in_read")
|
||||
outReadMessageId = o.optInt("out_read")
|
||||
lastMessageId = o.optInt("last_message_id", -1)
|
||||
unreadCount = o.optInt("unread_count", 0)
|
||||
|
||||
o.optJSONObject("peer")?.let {
|
||||
id = it.optInt("id", -1)
|
||||
type = Type.fromString(it.optString("type"))
|
||||
localId = it.optInt("local_id")
|
||||
}
|
||||
|
||||
o.optJSONObject("push_settings")?.let {
|
||||
disabledUntil = it.optInt("disabled_until")
|
||||
isDisabledForever = it.optBoolean("disabled_forever")
|
||||
isNoSound = it.optBoolean("no_sound")
|
||||
}
|
||||
|
||||
o.optJSONObject("can_write")?.let {
|
||||
isAllowed = it.optBoolean("allowed")
|
||||
notAllowedReason = Reason.fromInt(it.optInt("reason", -1))
|
||||
}
|
||||
|
||||
o.optJSONObject("chat_settings")?.let {
|
||||
membersCount = it.optInt("members_count")
|
||||
title = it.optString("title")
|
||||
if (title?.isBlank() == true) title = null
|
||||
|
||||
it.optJSONObject("pinned_message")?.let { pinned ->
|
||||
pinnedMessage = VKMessage(pinned)
|
||||
}
|
||||
|
||||
state = State.fromString(it.optString("state"))
|
||||
|
||||
it.optJSONObject("photo")?.let { photo ->
|
||||
photo50 = photo.optString("photo_50")
|
||||
photo100 = photo.optString("photo_100")
|
||||
photo200 = photo.optString("photo_200")
|
||||
}
|
||||
|
||||
isGroupChannel = it.optBoolean("is_group_channel")
|
||||
}
|
||||
}
|
||||
|
||||
fun isNotificationsDisabled() = (isDisabledForever || disabledUntil > 0 || isNoSound)
|
||||
|
||||
fun isChat() = type == Type.CHAT
|
||||
|
||||
fun isUser() = type == Type.USER
|
||||
|
||||
fun isGroup() = type == Type.GROUP
|
||||
|
||||
override fun toString() = title ?: ""
|
||||
|
||||
public override fun clone() = super.clone() as VKConversation
|
||||
|
||||
enum class Type(val value: String) {
|
||||
NULL("null"),
|
||||
USER("user"),
|
||||
CHAT("chat"),
|
||||
GROUP("group");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
enum class State(val value: String) {
|
||||
IN("in"),
|
||||
KICKED("kicked"),
|
||||
LEFT("left");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
enum class Reason(val value: Int) {
|
||||
NULL(-1),
|
||||
U(0),
|
||||
BLOCKED_DELETED(18),
|
||||
BLACKLISTED(900),
|
||||
BLOCKED_GROUP_MESSAGES(901),
|
||||
PRIVACY_SETTINGS(902),
|
||||
GROUP_DISABLED_MESSAGES(915),
|
||||
GROUP_BLOCKED_MESSAGES(916),
|
||||
NO_ACCESS_CHAT(917),
|
||||
NO_ACCESS_EMAIL(918),
|
||||
U1(925),
|
||||
NO_ACCESS_COMMUNITY(203);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
class VKDocument() : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.DOCUMENT
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
var id: Int = 0
|
||||
var ownerId: Int = 0
|
||||
var title: String = ""
|
||||
var size: Int = 0
|
||||
var ext: String = ""
|
||||
var url: String = ""
|
||||
var date: Int = 0
|
||||
var type: Type = Type.UNKNOWN
|
||||
var preview: Preview? = null
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
ownerId = o.optInt("owner_id", -1)
|
||||
title = o.optString("title")
|
||||
size = o.optInt("size")
|
||||
ext = o.optString("ext")
|
||||
url = o.optString("url")
|
||||
date = o.optInt("date")
|
||||
type = Type.fromInt(o.optInt("type"))
|
||||
|
||||
o.optJSONObject("preview")?.let {
|
||||
preview = Preview(it)
|
||||
}
|
||||
}
|
||||
|
||||
class Preview(o: JSONObject) : Serializable {
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
var photo: Photo? = null
|
||||
var graffiti: Graffiti? = null
|
||||
|
||||
inner class Photo(o: JSONObject) : Serializable {
|
||||
|
||||
var sizes: ArrayList<VKPhotoSize>? = null
|
||||
|
||||
init {
|
||||
o.optJSONArray("sizes")?.let {
|
||||
val sizes = ArrayList<VKPhotoSize>()
|
||||
for (i in 0 until it.length()) {
|
||||
sizes.add(VKPhotoSize(it.optJSONObject(i)))
|
||||
}
|
||||
this.sizes = sizes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Graffiti(o: JSONObject) : Serializable {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
var src: String = o.optString("src")
|
||||
var width: Int = o.optInt("width")
|
||||
var height: Int = o.optInt("height")
|
||||
}
|
||||
|
||||
init {
|
||||
o.optJSONObject("photo")?.let {
|
||||
photo = Photo(it)
|
||||
}
|
||||
|
||||
o.optJSONObject("graffiti")?.let {
|
||||
graffiti = Graffiti(it)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type(val value: Int) {
|
||||
NONE(0),
|
||||
TEXT(1),
|
||||
ARCHIVE(2),
|
||||
GIF(3),
|
||||
IMAGE(4),
|
||||
AUDIO(5),
|
||||
VIDEO(6),
|
||||
BOOK(7),
|
||||
UNKNOWN(8);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKGeolocation() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.GEOLOCATION
|
||||
|
||||
constructor(o: JSONObject) : this() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKGift() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.GIFT
|
||||
|
||||
var id: Int = 0
|
||||
var thumb256: String = ""
|
||||
var thumb96: String = ""
|
||||
var thumb48: String = ""
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
thumb256 = o.optString("thumb_256")
|
||||
thumb96 = o.optString("thumb_96")
|
||||
thumb48 = o.optString("thumb_48")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKGraffiti() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.GRAFFITI
|
||||
|
||||
var id: Int = 0
|
||||
var ownerId: Int = 0
|
||||
var url: String = ""
|
||||
var width: Int = 0
|
||||
var height: Int = 0
|
||||
var accessKey: String = ""
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
ownerId = o.optInt("owner_id", -1)
|
||||
url = o.optString("url")
|
||||
width = o.optInt("width")
|
||||
height = o.optInt("height")
|
||||
accessKey = o.optString("access_key")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
open class VKGroup() : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
companion object {
|
||||
|
||||
const val serialVersionUID: Long = 1L
|
||||
|
||||
fun parse(array: JSONArray): ArrayList<VKGroup> {
|
||||
val groups = ArrayList<VKGroup>()
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
groups.add(VKGroup(array.optJSONObject(i)))
|
||||
}
|
||||
return groups
|
||||
}
|
||||
}
|
||||
|
||||
var id: Int = 0
|
||||
var name: String = ""
|
||||
var screenName: String = ""
|
||||
var isClosed: Boolean = false
|
||||
var deactivated: String = ""
|
||||
var type: Type = Type.NULL
|
||||
var photo50: String = ""
|
||||
var photo100: String = ""
|
||||
var photo200: String = ""
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
name = o.optString("name")
|
||||
screenName = o.optString("screen_name")
|
||||
isClosed = o.optInt("is_closed") == 1
|
||||
deactivated = o.optString("deactivated")
|
||||
type = Type.fromString(o.optString("type"))
|
||||
photo50 = o.optString("photo_50")
|
||||
photo100 = o.optString("photo_100")
|
||||
photo200 = o.optString("photo_200")
|
||||
}
|
||||
|
||||
enum class Type(val value: String) {
|
||||
NULL("null"),
|
||||
GROUP("group"),
|
||||
PAGE("page"),
|
||||
EVENT("event");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.Serializable
|
||||
|
||||
class VKLink() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.LINK
|
||||
|
||||
var url: String = ""
|
||||
var title: String = ""
|
||||
var caption: String = ""
|
||||
var description: String = ""
|
||||
var previewPage: String = ""
|
||||
var previewUrl: String = ""
|
||||
var photo: VKPhoto? = null
|
||||
var button: Button? = null
|
||||
|
||||
constructor(o: JSONObject): this() {
|
||||
url = o.optString("url")
|
||||
title = o.optString("title")
|
||||
caption = o.optString("caption")
|
||||
description = o.optString("description")
|
||||
previewPage = o.optString("preview_page")
|
||||
previewUrl = o.optString("preview_url")
|
||||
|
||||
o.optJSONObject("photo")?.let {
|
||||
photo = VKPhoto(it)
|
||||
}
|
||||
|
||||
o.optJSONObject("button")?.let {
|
||||
button = Button(it)
|
||||
}
|
||||
}
|
||||
|
||||
class Button(o: JSONObject) : Serializable {
|
||||
var title: String = o.optString("title")
|
||||
var action: Action? = null
|
||||
|
||||
init {
|
||||
o.optJSONObject("action")?.let {
|
||||
action = Action(it)
|
||||
}
|
||||
}
|
||||
|
||||
class Action(o: JSONObject) : Serializable {
|
||||
|
||||
var type: String = o.optString("type")
|
||||
var url: String = o.optString("url")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
class VKLongPollHistory : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
private val lpMessages: ArrayList<VKMessage>? = null
|
||||
private val messages: ArrayList<VKMessage>? = null
|
||||
private val profiles: ArrayList<VKUser>? = null
|
||||
private val groups: ArrayList<VKGroup>? = null //TODO: использовать
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKLongPollServer() : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
var key: String = ""
|
||||
var server: String = ""
|
||||
var ts: Long = 0
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
key = o.optString("key")
|
||||
server = o.optString("server").replace("\\", "")
|
||||
ts = o.optLong("ts")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.util.ArrayMap
|
||||
import com.meloda.fast.api.util.VKUtil
|
||||
import org.json.JSONObject
|
||||
|
||||
open class VKMessage() : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
companion object {
|
||||
|
||||
var profiles = arrayListOf<VKUser>()
|
||||
var groups = arrayListOf<VKGroup>()
|
||||
var conversations = arrayListOf<VKConversation>()
|
||||
|
||||
const val serialVersionUID: Long = 1L
|
||||
|
||||
var lastHistoryCount: Int = 0
|
||||
|
||||
const val UNREAD = 1 // Оно просто есть
|
||||
const val OUTBOX = 1 shl 1 // Исходящее сообщение
|
||||
const val REPLIED = 1 shl 2 // На сообщение был создан ответ
|
||||
const val IMPORTANT = 1 shl 3 // Важное сообщение
|
||||
const val FRIENDS = 1 shl 5 // Сообщение в чат друга
|
||||
const val SPAM = 1 shl 6 // Сообщение помечено как спам
|
||||
const val DELETED = 1 shl 7 // Удаление сообщения
|
||||
const val AUDIO_LISTENED = 1 shl 12 // ГС прослушано
|
||||
const val CHAT = 1 shl 13 // Сообщение отправлено в беседу
|
||||
const val CANCEL_SPAM = 1 shl 15 // Отмена пометки спама
|
||||
const val HIDDEN = 1 shl 16 // Приветственное сообщение сообщества
|
||||
const val DELETE_FOR_ALL = 1 shl 17 // Сообщение удалено для всех
|
||||
const val CHAT_IN = 1 shl 19 // Входящее сообщение в беседе
|
||||
const val REPLY_MSG = 1 shl 21 // Ответ на сообщение
|
||||
|
||||
val flags = ArrayMap<String, Int>()
|
||||
|
||||
fun isOut(flags: Int): Boolean {
|
||||
return OUTBOX and flags > 0
|
||||
}
|
||||
|
||||
fun isDeleted(flags: Int): Boolean {
|
||||
return DELETED and flags > 0
|
||||
}
|
||||
|
||||
fun isUnread(flags: Int): Boolean {
|
||||
return UNREAD and flags > 0
|
||||
}
|
||||
|
||||
fun isSpam(flags: Int): Boolean {
|
||||
return SPAM and flags > 0
|
||||
}
|
||||
|
||||
fun isCanceledSpam(flags: Int): Boolean {
|
||||
return CANCEL_SPAM and flags > 0
|
||||
}
|
||||
|
||||
fun isImportant(flags: Int): Boolean {
|
||||
return IMPORTANT and flags > 0
|
||||
}
|
||||
|
||||
fun isDeletedForAll(flags: Int): Boolean {
|
||||
return DELETE_FOR_ALL and flags > 0
|
||||
}
|
||||
|
||||
init {
|
||||
flags["unread"] = UNREAD
|
||||
flags["outbox"] = OUTBOX
|
||||
flags["replied"] = REPLIED
|
||||
flags["important"] = IMPORTANT
|
||||
flags["friends"] = FRIENDS
|
||||
flags["spam"] = SPAM
|
||||
flags["deleted"] = DELETED
|
||||
flags["audio_listened"] = AUDIO_LISTENED
|
||||
flags["chat"] = CHAT
|
||||
flags["cancel_spam"] = CANCEL_SPAM
|
||||
flags["hidden"] = HIDDEN
|
||||
flags["delete_for_all"] = DELETE_FOR_ALL
|
||||
flags["chat_in"] = CHAT_IN
|
||||
flags["reply_msg"] = REPLY_MSG
|
||||
}
|
||||
}
|
||||
|
||||
var id: Int = 0
|
||||
var date: Int = 0
|
||||
var peerId: Int = 0
|
||||
var fromId: Int = 0
|
||||
var editTime: Int = 0
|
||||
var isOut: Boolean = false
|
||||
var text: String = ""
|
||||
var randomId: Int = 0
|
||||
var conversationMessageId: Int = 0
|
||||
|
||||
var hasEmoji: Boolean = false
|
||||
var isImportant: Boolean = false
|
||||
var isRead: Boolean = false
|
||||
|
||||
var attachments: ArrayList<VKModel> = arrayListOf()
|
||||
|
||||
var fwdMessages: ArrayList<VKMessage> = arrayListOf()
|
||||
|
||||
var replyMessage: VKMessage? = null
|
||||
|
||||
var action: VKMessageAction? = null
|
||||
|
||||
var fromUser: VKUser? = null
|
||||
|
||||
var fromGroup: VKGroup? = null
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
date = o.optInt("date")
|
||||
peerId = o.optInt("peer_id", -1)
|
||||
fromId = o.optInt("from_id", -1)
|
||||
editTime = o.optInt("edit_time", -1)
|
||||
isOut = o.optInt("out") == 1
|
||||
|
||||
text = VKUtil.prepareMessageText(o.optString("text"))
|
||||
|
||||
randomId = o.optInt("random_id", -1)
|
||||
conversationMessageId = o.optInt("conversation_message_id", -1)
|
||||
isImportant = o.optBoolean("important")
|
||||
|
||||
o.optJSONArray("attachments")?.let {
|
||||
attachments = VKAttachments.parse(it)
|
||||
}
|
||||
|
||||
o.optJSONArray("fwd_messages")?.let {
|
||||
val fwdMessages = ArrayList<VKMessage>(it.length())
|
||||
for (i in 0 until it.length()) {
|
||||
fwdMessages.add(VKMessage(it.optJSONObject(i)))
|
||||
}
|
||||
this.fwdMessages = fwdMessages
|
||||
}
|
||||
|
||||
o.optJSONObject("reply_message")?.let {
|
||||
replyMessage = VKMessage(it)
|
||||
}
|
||||
|
||||
o.optJSONObject("action")?.let {
|
||||
action = VKMessageAction(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun getForwardedMessages() = ArrayList<VKMessage>().apply {
|
||||
for (model in fwdMessages) add(model)
|
||||
}
|
||||
|
||||
fun isFromUser() = fromId > 0
|
||||
|
||||
fun isFromGroup() = fromId < 0
|
||||
|
||||
fun isOutbox() = isOut
|
||||
|
||||
fun isInbox() = !isOutbox()
|
||||
|
||||
override fun toString(): String {
|
||||
return if (text.isNotEmpty()) {
|
||||
text
|
||||
} else {
|
||||
super.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKMessageAction() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
var type: Type = Type.NONE
|
||||
var memberId = 0
|
||||
var message: VKMessage? = null
|
||||
var conversationMessageId: Int = 0
|
||||
var text: String = ""
|
||||
var oldText: String = ""
|
||||
|
||||
//TODO: add photo
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
type = Type.fromString(o.optString("type"))
|
||||
memberId = o.optInt("member_id", -1)
|
||||
text = o.optString("text")
|
||||
}
|
||||
|
||||
enum class Type(val value: String) {
|
||||
NONE("none"),
|
||||
CHAT_CREATE("chat_create"),
|
||||
PHOTO_UPDATE("chat_photo_update"),
|
||||
PHOTO_REMOVE("chat_photo_remove"),
|
||||
TITLE_UPDATE("chat_title_update"),
|
||||
PIN_MESSAGE("chat_pin_message"),
|
||||
UNPIN_MESSAGE("chat_unpin_message"),
|
||||
INVITE_USER("chat_invite_user"),
|
||||
INVITE_USER_BY_LINK("chat_invite_user_by_link"),
|
||||
KICK_USER("chat_kick_user"),
|
||||
SCREENSHOT("chat_screenshot"),
|
||||
INVITE_USER_BY_CALL("chat_invite_user_by_call"),
|
||||
INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_link");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import com.meloda.fast.base.adapter.BaseItem
|
||||
import java.io.Serializable
|
||||
|
||||
abstract class VKModel : BaseItem(), Serializable {
|
||||
|
||||
abstract val attachmentType: VKAttachments.Type
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID = 1L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class VKPhoto() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.PHOTO
|
||||
|
||||
var id: Int = 0
|
||||
var albumId: Int = 0
|
||||
var ownerId: Int = 0
|
||||
var text: String = ""
|
||||
var date: Int = 0
|
||||
var width: Int = 0
|
||||
var height: Int = 0
|
||||
var sizes: ArrayList<VKPhotoSize>? = null
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id", -1)
|
||||
albumId = o.optInt("album_id", -1)
|
||||
ownerId = o.optInt("owner_id", -1)
|
||||
text = o.optString("text")
|
||||
date = o.optInt("date")
|
||||
width = o.optInt("width")
|
||||
height = o.optInt("height")
|
||||
|
||||
o.optJSONArray("sizes")?.let {
|
||||
val sizes = ArrayList<VKPhotoSize>()
|
||||
for (i in 0 until it.length()) {
|
||||
sizes.add(VKPhotoSize(it.optJSONObject(i)))
|
||||
}
|
||||
this.sizes = sizes
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKPhotoSize(o: JSONObject) : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
var type: String = o.optString("type")
|
||||
var url: String = o.optString("url")
|
||||
var height: Int = o.optInt("height")
|
||||
var width: Int = o.optInt("width")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKPoll() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.POLL
|
||||
|
||||
constructor(o: JSONObject): this() {}
|
||||
|
||||
// var id = o.optInt("id", -1)
|
||||
// var ownerId = o.optInt("owner_id", -1)
|
||||
// var created = o.optInt("created")
|
||||
// var question: String = o.optString("question")
|
||||
// var votes = o.optInt("votes")
|
||||
// var answers = ArrayList<Answer>()
|
||||
// var isAnonymous = o.optBoolean("anonymous")
|
||||
// var isMultiple = o.optBoolean("multiple")
|
||||
// var answerIds = ArrayList<Int>()
|
||||
// var endDate = o.optInt("end_date")
|
||||
// var isClosed = o.optBoolean("closed")
|
||||
// var isBoard = o.optBoolean("is_board")
|
||||
// var isCanEdit = o.optBoolean("can_edit")
|
||||
// var isCanVote = false
|
||||
// var isCanReport = false
|
||||
// var isCanShare = false
|
||||
// var authorId = 0
|
||||
// var background = Color.WHITE
|
||||
|
||||
//TODO: private ArrayList friends
|
||||
|
||||
// init {
|
||||
// o.optJSONArray("answers")?.let {
|
||||
// val answers = ArrayList<Answer>()
|
||||
// for (i in 0 until it.length()) {
|
||||
// answers.add(Answer(it.optJSONObject(i)))
|
||||
// }
|
||||
// this.answers = answers
|
||||
// }
|
||||
|
||||
// //setAnswerIds();
|
||||
|
||||
// // ...
|
||||
// }
|
||||
|
||||
// class Answer(o: JSONObject) : Serializable {
|
||||
|
||||
// var id = o.optInt("id", -1)
|
||||
// var text: String = o.optString("text")
|
||||
// var votes = o.optInt("votes")
|
||||
// var rate = o.optInt("rate")
|
||||
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class VKSticker() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.STICKER
|
||||
|
||||
var productId: Int = 0
|
||||
var stickerId: Int = 0
|
||||
var images: ArrayList<Image>? = null
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
productId = o.optInt("product_id", -1)
|
||||
stickerId = o.optInt("sticker_id", -1)
|
||||
|
||||
o.optJSONArray("images")?.let {
|
||||
val images = ArrayList<Image>()
|
||||
for (i in 0 until it.length()) {
|
||||
images.add(Image(it.optJSONObject(i)))
|
||||
}
|
||||
this.images = images
|
||||
}
|
||||
}
|
||||
|
||||
class Image(o: JSONObject) : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
var url: String = o.optString("url")
|
||||
var width = o.optInt("width")
|
||||
var height = o.optInt("height")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
open class VKUser() : VKModel() {
|
||||
|
||||
override val attachmentType = VKAttachments.Type.NONE
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
|
||||
var friendsCount: Int = 0
|
||||
|
||||
fun parse(array: JSONArray): ArrayList<VKUser> {
|
||||
val users = ArrayList<VKUser>()
|
||||
|
||||
for (i in 0 until array.length()) {
|
||||
users.add(VKUser(array.optJSONObject(i)))
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: Int = 0
|
||||
|
||||
var userId: Int = 0
|
||||
var firstName: String = ""
|
||||
var lastName: String = ""
|
||||
var deactivated: String = ""
|
||||
var isClosed: Boolean = false
|
||||
var isCanAccessClosed: Boolean = true
|
||||
var sex: Int = 0
|
||||
var screenName: String = ""
|
||||
var photo50: String = ""
|
||||
var photo100: String = ""
|
||||
var photo200: String = ""
|
||||
var isOnline: Boolean = false
|
||||
var isOnlineMobile: Boolean = false
|
||||
var status: String = ""
|
||||
|
||||
var lastSeen: Int = 0
|
||||
var lastSeenPlatform: Int = 0
|
||||
|
||||
var isVerified: Boolean = false
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
sortId = 0
|
||||
userId = o.optInt("id", -1)
|
||||
firstName = o.optString("first_name")
|
||||
lastName = o.optString("last_name")
|
||||
deactivated = o.optString("deactivated", "")
|
||||
isClosed = o.optBoolean("is_closed")
|
||||
isCanAccessClosed = o.optBoolean("can_access_closed")
|
||||
sex = o.optInt("sex")
|
||||
screenName = o.optString("screen_name")
|
||||
photo50 = o.optString("photo_50")
|
||||
photo100 = o.optString("photo_100")
|
||||
photo200 = o.optString("photo_200")
|
||||
isOnline = o.optInt("online") == 1
|
||||
isOnlineMobile = isOnline && o.optInt("online_mobile") == 1
|
||||
status = o.optString("status")
|
||||
lastSeen = 0
|
||||
lastSeenPlatform = 0
|
||||
isVerified = o.optInt("verified") == 1
|
||||
|
||||
o.optJSONObject("last_seen")?.let {
|
||||
lastSeen = it.optInt("time")
|
||||
lastSeenPlatform = it.optInt("platform")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun isDeactivated() = deactivated.isNotEmpty()
|
||||
|
||||
override fun toString(): String {
|
||||
return "$firstName $lastName"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKVideo() : VKModel() {
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.VIDEO
|
||||
|
||||
// var id = o.optInt("id", -1)
|
||||
// var ownerId = o.optInt("owner_id", -1)
|
||||
// var title: String = o.optString("title")
|
||||
// var description: String = o.optString("description")
|
||||
// var duration = o.optInt("duration", -1)
|
||||
// var photo130: String = o.optString("photo_130")
|
||||
// var photo320: String = o.optString("photo_320")
|
||||
// var photo640: String = o.optString("photo_640")
|
||||
// var photo800: String = o.optString("photo_800")
|
||||
// var photo1280: String = o.optString("photo_1280")
|
||||
// var firstFrame130: String = o.optString("first_frame_130")
|
||||
// var firstFrame320: String = o.optString("first_frame_320")
|
||||
// var firstFrame640: String = o.optString("first_frame_640")
|
||||
// var firstFrame800: String = o.optString("first_frame_800")
|
||||
// var firstFrame1280: String = o.optString("first_frame_1280")
|
||||
// var date = o.optInt("date")
|
||||
// var views = o.optInt("views")
|
||||
// var comments = o.optInt("comments")
|
||||
// var player: String = o.optString("player")
|
||||
// var isCanEdit = o.optInt("can_edit", 0) == 1
|
||||
// var isCanAdd = o.optInt("can_add") == 1
|
||||
// var isPrivate = o.optInt("is_private", 0) == 1
|
||||
// var accessKey: String = o.optString("access_key")
|
||||
// var isProcessing = o.optInt("processing", 0) == 1
|
||||
// var isLive = o.optInt("live", 0) == 1
|
||||
// var isUpcoming = o.optInt("upcoming", 0) == 1
|
||||
// var isFavorite = o.optBoolean("favorite")
|
||||
|
||||
constructor(o: JSONObject) : this() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class VKWall() : VKModel() { //https://vk.com/dev/objects/post
|
||||
|
||||
companion object {
|
||||
const val serialVersionUID: Long = 1L
|
||||
}
|
||||
|
||||
override val attachmentType = VKAttachments.Type.WALL_POST
|
||||
|
||||
constructor(o: JSONObject) : this() {}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.meloda.fast.api.model.request
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class RequestMessagesGetConversations(
|
||||
@SerializedName("offset")
|
||||
private val offset: Int = 0,
|
||||
|
||||
@SerializedName("count")
|
||||
private val count: Int = 0,
|
||||
|
||||
//values = all, unread
|
||||
@SerializedName("filter")
|
||||
private val filter: String = "",
|
||||
|
||||
@SerializedName("extended")
|
||||
private val extended: Boolean = false,
|
||||
|
||||
@SerializedName("fields")
|
||||
private var fields: String = ""
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.meloda.fast.api.model.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class MessagesResponse(
|
||||
val count: Int
|
||||
) {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class GetConversationsResponse(val a: String) : Parcelable
|
||||
// TODO: 7/12/2021 use hilt for this like in LIR and make simple conversations' screen
|
||||
@@ -0,0 +1,13 @@
|
||||
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>>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.meloda.fast.api.util
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.api.model.*
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object VKUtil {
|
||||
|
||||
private const val TAG = "VKUtil"
|
||||
|
||||
fun extractPattern(string: String, pattern: String): String? {
|
||||
val p = Pattern.compile(pattern)
|
||||
val m = p.matcher(string)
|
||||
return if (!m.find()) null else m.toMatchResult().group(1)
|
||||
}
|
||||
|
||||
private const val pattern_string_profile_id = "^(id)?(\\d{1,10})$"
|
||||
|
||||
private val pattern_profile_id = Pattern.compile(pattern_string_profile_id)
|
||||
|
||||
fun parseProfileId(text: String): String? {
|
||||
val m = pattern_profile_id.matcher(text)
|
||||
return if (!m.find()) null else m.group(2)
|
||||
}
|
||||
|
||||
fun sortMessagesByDate(
|
||||
values: ArrayList<VKMessage>,
|
||||
firstOnTop: Boolean
|
||||
): ArrayList<VKMessage> {
|
||||
values.sortWith { m1, m2 ->
|
||||
val d1 = m1.date
|
||||
val d2 = m2.date
|
||||
|
||||
if (firstOnTop) {
|
||||
d2 - d1
|
||||
} else {
|
||||
d1 - d2
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
fun sortConversationsByDate(
|
||||
values: ArrayList<VKConversation>,
|
||||
firstOnTop: Boolean
|
||||
): ArrayList<VKConversation> {
|
||||
values.sortWith { c1, c2 ->
|
||||
val d1 = c1.lastMessage.date
|
||||
val d2 = c2.lastMessage.date
|
||||
|
||||
return@sortWith if (firstOnTop) {
|
||||
d2 - d1
|
||||
} else {
|
||||
d1 - d2
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
fun prepareMessageText(message: String): String {
|
||||
if (message.isEmpty()) return message
|
||||
|
||||
var newText = message
|
||||
|
||||
val mentions = hashMapOf<String, String>()
|
||||
|
||||
var startFrom = 0
|
||||
|
||||
while (true) {
|
||||
val leftBracketIndex = newText.indexOf('[', startFrom)
|
||||
val verticalLineIndex = newText.indexOf('|', startFrom)
|
||||
val rightBracketIndex = newText.indexOf(']', startFrom)
|
||||
|
||||
if (leftBracketIndex == -1 ||
|
||||
verticalLineIndex == -1 ||
|
||||
rightBracketIndex == -1
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
val id = newText.substring(leftBracketIndex + 1, verticalLineIndex)
|
||||
|
||||
if (!id.matches(Regex("^id(\\d+)\$")) || rightBracketIndex - verticalLineIndex < 2) {
|
||||
break
|
||||
}
|
||||
|
||||
val text = newText.substring(verticalLineIndex + 1, rightBracketIndex)
|
||||
|
||||
val str = "[$id|$text]"
|
||||
|
||||
mentions[str] = text
|
||||
startFrom = rightBracketIndex + 1
|
||||
}
|
||||
|
||||
mentions.forEach {
|
||||
newText = newText.replace(it.key, it.value)
|
||||
}
|
||||
|
||||
return newText
|
||||
}
|
||||
|
||||
// fun removeTime(date: Date): Long {
|
||||
// return Calendar.getInstance().apply {
|
||||
// time = date
|
||||
// this[Calendar.HOUR_OF_DAY] = 0
|
||||
// this[Calendar.MINUTE] = 0
|
||||
// this[Calendar.SECOND] = 0
|
||||
// this[Calendar.MILLISECOND] = 0
|
||||
// }.timeInMillis
|
||||
// }
|
||||
|
||||
|
||||
//TODO: нормальное время
|
||||
fun getLastSeenTime(date: Long): String {
|
||||
return SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
}
|
||||
|
||||
|
||||
fun getTitle(
|
||||
conversation: VKConversation,
|
||||
peerUser: VKUser?,
|
||||
peerGroup: VKGroup?
|
||||
): String {
|
||||
return when {
|
||||
conversation.isUser() -> peerUser?.let { return it.toString() } ?: ""
|
||||
|
||||
|
||||
conversation.isGroup() -> peerGroup?.let { return it.name } ?: ""
|
||||
|
||||
|
||||
conversation.isChat() -> conversation.title ?: ""
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessageTitle(
|
||||
message: VKMessage,
|
||||
fromUser: VKUser?,
|
||||
fromGroup: VKGroup?
|
||||
): String {
|
||||
return when {
|
||||
message.isFromUser() -> {
|
||||
fromUser?.let { return it.toString() } ?: ""
|
||||
}
|
||||
|
||||
message.isFromGroup() -> {
|
||||
fromGroup?.let { return it.name } ?: ""
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvatar(
|
||||
conversation: VKConversation,
|
||||
peerUser: VKUser?,
|
||||
peerGroup: VKGroup?
|
||||
): String {
|
||||
return when {
|
||||
conversation.isUser() -> {
|
||||
peerUser?.let { return it.photo200 } ?: ""
|
||||
}
|
||||
|
||||
conversation.isGroup() -> {
|
||||
peerGroup?.let { return it.photo200 } ?: ""
|
||||
}
|
||||
|
||||
conversation.isChat() -> {
|
||||
conversation.photo200
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserAvatar(
|
||||
message: VKMessage,
|
||||
fromUser: VKUser?,
|
||||
fromGroup: VKGroup?
|
||||
): String {
|
||||
return when {
|
||||
message.isFromUser() -> {
|
||||
fromUser?.let { return it.photo100 } ?: ""
|
||||
}
|
||||
|
||||
message.isFromGroup() -> {
|
||||
fromGroup?.let { return it.photo100 } ?: ""
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserPhoto(user: VKUser): String {
|
||||
if (user.photo200.isEmpty()) {
|
||||
if (user.photo100.isEmpty()) {
|
||||
if (user.photo50.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return user.photo100
|
||||
}
|
||||
} else {
|
||||
return user.photo200
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
fun getGroupPhoto(group: VKGroup): String {
|
||||
if (group.photo200.isEmpty()) {
|
||||
if (group.photo100.isEmpty()) {
|
||||
if (group.photo50.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return group.photo100
|
||||
}
|
||||
} else {
|
||||
return group.photo200
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
fun parseConversations(array: JSONArray): ArrayList<VKConversation> {
|
||||
val conversations = arrayListOf<VKConversation>()
|
||||
for (i in 0 until array.length()) {
|
||||
conversations.add(VKConversation(array.optJSONObject(i)))
|
||||
}
|
||||
|
||||
return conversations
|
||||
}
|
||||
|
||||
fun parseMessages(array: JSONArray): ArrayList<VKMessage> {
|
||||
val messages = arrayListOf<VKMessage>()
|
||||
for (i in 0 until array.length()) {
|
||||
messages.add(VKMessage(array.optJSONObject(i)))
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
fun isMessageHasFlag(mask: Int, flagName: String): Boolean {
|
||||
val o: Any? = VKMessage.flags[flagName]
|
||||
return if (o != null) { //has flag
|
||||
val flag = o as Int
|
||||
flag and mask > 0
|
||||
} else false
|
||||
}
|
||||
|
||||
//TODO: rewrite parsing
|
||||
//fromUser and fromGroup are null
|
||||
@Deprecated("need to rewrite")
|
||||
@WorkerThread
|
||||
fun parseLongPollMessage(array: JSONArray): VKMessage {
|
||||
val message = VKMessage()
|
||||
|
||||
val id = array.optInt(1)
|
||||
val flags = array.optInt(2)
|
||||
val peerId = array.optInt(3)
|
||||
val date = array.optInt(4)
|
||||
val text = array.optString(5)
|
||||
|
||||
message.id = id
|
||||
message.peerId = peerId
|
||||
message.date = date
|
||||
message.text = text
|
||||
|
||||
// val fromId =
|
||||
// if (isMessageHasFlag(flags, "outbox")) com.meloda.fast.UserConfig.userId
|
||||
// else peerId
|
||||
|
||||
message.fromId = peerId
|
||||
|
||||
array.optJSONObject(6)?.let {
|
||||
if (it.has("emoji")) message.hasEmoji = true
|
||||
|
||||
if (it.has("from")) {
|
||||
message.fromId = it.optInt("from", -1)
|
||||
}
|
||||
|
||||
if (it.has("source_act")) {
|
||||
message.action = VKMessageAction().also { action ->
|
||||
action.type =
|
||||
VKMessageAction.Type.fromString(it.optString("source_act"))
|
||||
|
||||
when (action.type) {
|
||||
VKMessageAction.Type.CHAT_CREATE -> {
|
||||
action.text = it.optString("source_text")
|
||||
}
|
||||
VKMessageAction.Type.TITLE_UPDATE -> {
|
||||
action.oldText = it.optString("source_old_text")
|
||||
action.text = it.optString("source_text")
|
||||
}
|
||||
VKMessageAction.Type.PIN_MESSAGE -> {
|
||||
action.memberId = it.optInt("source_mid")
|
||||
action.conversationMessageId = it.optInt("source_chat_local_id")
|
||||
|
||||
it.optJSONObject("source_message")?.let { message ->
|
||||
action.message = VKMessage(message)
|
||||
}
|
||||
}
|
||||
VKMessageAction.Type.UNPIN_MESSAGE -> {
|
||||
action.memberId = it.optInt("source_mid")
|
||||
action.conversationMessageId = it.optInt("source_chat_local_id")
|
||||
}
|
||||
VKMessageAction.Type.INVITE_USER,
|
||||
VKMessageAction.Type.KICK_USER,
|
||||
VKMessageAction.Type.SCREENSHOT,
|
||||
VKMessageAction.Type.INVITE_USER_BY_CALL -> {
|
||||
action.memberId = it.optInt("source_mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array.optJSONObject(7)?.let {
|
||||
/**
|
||||
*
|
||||
* fwd? reply? attachments_count? attachments?
|
||||
*
|
||||
*/
|
||||
}
|
||||
|
||||
val randomId = array.optInt(8)
|
||||
message.randomId = randomId
|
||||
|
||||
val conversationMessageId = array.optInt(9)
|
||||
message.conversationMessageId = conversationMessageId
|
||||
|
||||
val editTime = array.optInt(10)
|
||||
message.editTime = editTime
|
||||
|
||||
// val out = fromId == com.meloda.fast.UserConfig.userId
|
||||
// message.isOut = out
|
||||
//
|
||||
// if (message.isFromUser()) {
|
||||
// message.fromUser = MemoryCache.getUserById(fromId)
|
||||
// } else {
|
||||
// message.fromGroup = MemoryCache.getGroupById(abs(fromId))
|
||||
// }
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
fun parseJsonPhotos(jsonPhotos: JSONObject): List<String> {
|
||||
val photos = arrayListOf<String>()
|
||||
|
||||
for (key in jsonPhotos.keys()) {
|
||||
photos.add(jsonPhotos.getString(key))
|
||||
}
|
||||
|
||||
return photos
|
||||
}
|
||||
|
||||
fun putPhotosToJson(photo50: String, photo100: String, photo200: String): JSONObject {
|
||||
val json = JSONObject()
|
||||
|
||||
json.put("photo_50", photo50)
|
||||
json.put("photo_100", photo100)
|
||||
json.put("photo_200", photo200)
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
fun isGroupId(id: Int) = id < 0
|
||||
|
||||
fun isUserId(id: Int) = id in 1..1999999999
|
||||
|
||||
fun isChatId(id: Int) = id > 2_000_000_000
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(@LayoutRes resId: Int) : super(resId)
|
||||
|
||||
protected lateinit var lifecycleRegistry: LifecycleRegistry
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleRegistry = LifecycleRegistry(this)
|
||||
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.STARTED
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
||||
}
|
||||
|
||||
val rootView: View? get() = findViewById(android.R.id.content)
|
||||
|
||||
fun requireRootView() = rootView!!
|
||||
|
||||
var errorSnackbar: Snackbar? = null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class BaseFragment : Fragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(@LayoutRes resId: Int) : super(resId)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.meloda.fast.R
|
||||
|
||||
abstract class BaseFullscreenDialog : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NORMAL, R.style.AppTheme_FullScreenDialog)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
dialog?.let { dialog ->
|
||||
val width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
val height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
dialog.window?.let {
|
||||
it.setLayout(width, height)
|
||||
it.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
||||
it.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
|
||||
it.setWindowAnimations(R.style.AppTheme_Slide)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
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.VKEvent
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(@LayoutRes resId: Int) : super(resId)
|
||||
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
viewModel.tasksEvent.onEach { onEvent(it) }.collect()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onEvent(event: VKEvent) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "CanBeParameter")
|
||||
abstract class BaseAdapter<Item : BaseItem, VH : BaseHolder>(
|
||||
var context: Context,
|
||||
values: ArrayList<Item>,
|
||||
diffUtil: DiffUtil.ItemCallback<Item>
|
||||
) : ListAdapter<Item, VH>(diffUtil) {
|
||||
|
||||
val cleanValues = arrayListOf<Item>()
|
||||
val values = arrayListOf<Item>()
|
||||
|
||||
init {
|
||||
addAll(values)
|
||||
}
|
||||
|
||||
protected var inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
var itemClickListener: OnItemClickListener? = null
|
||||
var itemLongClickListener: OnItemLongClickListener? = null
|
||||
|
||||
open fun destroy() {
|
||||
itemClickListener = null
|
||||
itemLongClickListener = null
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Item {
|
||||
return values[position]
|
||||
}
|
||||
|
||||
fun add(position: Int, item: Item) {
|
||||
values.add(position, item)
|
||||
cleanValues.add(position, item)
|
||||
}
|
||||
|
||||
fun add(item: Item) {
|
||||
values += item
|
||||
cleanValues.add(item)
|
||||
}
|
||||
|
||||
fun addAll(items: List<Item>) {
|
||||
values += items
|
||||
cleanValues.addAll(items)
|
||||
}
|
||||
|
||||
fun addAll(position: Int, items: List<Item>) {
|
||||
values.addAll(position, items)
|
||||
cleanValues.addAll(position, items)
|
||||
}
|
||||
|
||||
fun removeAll(items: List<Item>) {
|
||||
values.removeAll(items)
|
||||
cleanValues.removeAll(items)
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
values.removeAt(index)
|
||||
cleanValues.removeAt(index)
|
||||
}
|
||||
|
||||
fun remove(item: Item) {
|
||||
values.remove(item)
|
||||
cleanValues.remove(item)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
values.clear()
|
||||
cleanValues.clear()
|
||||
}
|
||||
|
||||
operator fun get(position: Int): Item {
|
||||
return values[position]
|
||||
}
|
||||
|
||||
operator fun set(position: Int, item: Item) {
|
||||
values[position] = item
|
||||
cleanValues[position] = item
|
||||
}
|
||||
|
||||
open fun notifyChanges(oldList: List<Item>, newList: List<Item>) {}
|
||||
|
||||
fun isEmpty() = values.isEmpty()
|
||||
fun isNotEmpty() = values.isNotEmpty()
|
||||
|
||||
fun view(resId: Int, viewGroup: ViewGroup, attachToRoot: Boolean = false): View {
|
||||
return inflater.inflate(resId, viewGroup, attachToRoot)
|
||||
}
|
||||
|
||||
fun updateValues(arrayList: ArrayList<Item>) {
|
||||
values.clear()
|
||||
values += arrayList
|
||||
}
|
||||
|
||||
fun updateValues(list: List<Item>) = updateValues(ArrayList(list))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
onBindItemViewHolder(holder, position)
|
||||
}
|
||||
|
||||
protected fun initListeners(itemView: View, position: Int) {
|
||||
if (itemView is AdapterView<*>) return
|
||||
|
||||
itemView.setOnClickListener {
|
||||
itemClickListener?.onItemClick(position)
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
itemLongClickListener?.onItemLongClick(position)
|
||||
return@setOnLongClickListener itemClickListener == null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return values.size
|
||||
}
|
||||
|
||||
val size get() = itemCount
|
||||
|
||||
private fun onBindItemViewHolder(holder: VH, position: Int) {
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
abstract class BaseItem
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class EmptyHeaderAdapter(
|
||||
var context: Context
|
||||
) : RecyclerView.Adapter<EmptyHeaderAdapter.Holder>() {
|
||||
|
||||
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(generateHeaderView())
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
}
|
||||
|
||||
override fun getItemCount() = 1
|
||||
|
||||
private fun generateHeaderView() = View(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(56).roundToInt()
|
||||
)
|
||||
isClickable = false
|
||||
isEnabled = false
|
||||
isFocusable = false
|
||||
isInvisible = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
open fun bind(position: Int) {}
|
||||
|
||||
open fun bind(position: Int, payloads: MutableList<Any>?) {}
|
||||
|
||||
}
|
||||
|
||||
abstract class BindingHolder<B : ViewBinding>(protected val binding: B) : BaseHolder(binding.root)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
|
||||
interface OnItemLongClickListener {
|
||||
fun onItemLongClick(position: Int)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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,11 @@
|
||||
package com.meloda.fast.base.viewmodel
|
||||
|
||||
data class ShowDialogInfoEvent(
|
||||
val title: String? = null,
|
||||
val message: String,
|
||||
val positiveBtn: String? = null,
|
||||
val negativeBtn: String? = null
|
||||
) : VKEvent()
|
||||
|
||||
object StartProgressEvent : VKEvent()
|
||||
object StopProgressEvent : VKEvent()
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.meloda.fast.base.viewmodel
|
||||
|
||||
abstract class VKEvent
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Handler
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.database.DatabaseHelper
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportingInteractionMode
|
||||
import org.acra.annotation.ReportsCrashes
|
||||
import java.util.*
|
||||
|
||||
@SuppressLint("NonConstantResourceId")
|
||||
@ReportsCrashes(
|
||||
mailTo = "lischenkodev@gmail.com",
|
||||
mode = ReportingInteractionMode.DIALOG,
|
||||
resDialogTitle = R.string.app_has_been_crashed,
|
||||
resDialogText = R.string.empty,
|
||||
resDialogTheme = R.style.AppTheme_Dialog,
|
||||
resDialogPositiveButtonText = R.string.send_crash_report,
|
||||
resDialogNegativeButtonText = R.string.ok
|
||||
)
|
||||
@HiltAndroidApp
|
||||
class AppGlobal : Application() {
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var inputMethodManager: InputMethodManager
|
||||
|
||||
lateinit var preferences: SharedPreferences
|
||||
lateinit var locale: Locale
|
||||
lateinit var handler: Handler
|
||||
lateinit var resources: Resources
|
||||
lateinit var packageName: String
|
||||
lateinit var instance: AppGlobal
|
||||
|
||||
lateinit var dbHelper: DatabaseHelper
|
||||
lateinit var database: SQLiteDatabase
|
||||
|
||||
lateinit var packageManager: PackageManager
|
||||
|
||||
var versionName = ""
|
||||
var versionCode = 0L
|
||||
|
||||
var screenWidth = 0
|
||||
var screenHeight = 0
|
||||
|
||||
fun post(runnable: Runnable) {
|
||||
handler.post(runnable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
ACRA.init(this)
|
||||
}
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
handler = Handler(mainLooper)
|
||||
locale = Locale.getDefault()
|
||||
|
||||
dbHelper = DatabaseHelper(this)
|
||||
database = dbHelper.writableDatabase
|
||||
|
||||
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
|
||||
versionName = info.versionName
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(info)
|
||||
|
||||
Companion.resources = resources
|
||||
Companion.packageName = packageName
|
||||
Companion.packageManager = packageManager
|
||||
|
||||
screenWidth = AndroidUtils.getDisplayWidth()
|
||||
screenHeight = AndroidUtils.getDisplayHeight()
|
||||
|
||||
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.meloda.fast.R
|
||||
|
||||
object FragmentSwitcher {
|
||||
|
||||
fun getCurrentFragment(fragmentManager: FragmentManager): Fragment? {
|
||||
val fragments = fragmentManager.fragments
|
||||
|
||||
if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty")
|
||||
|
||||
for (fragment in fragments) {
|
||||
if (fragment.isVisible) {
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun addFragments(
|
||||
fragmentManager: FragmentManager,
|
||||
containerId: Int,
|
||||
fragments: Collection<Fragment>
|
||||
) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
for (fragment in fragments) {
|
||||
transaction.add(containerId, fragment, fragment.javaClass.simpleName)
|
||||
}
|
||||
|
||||
transaction.commitNow()
|
||||
}
|
||||
|
||||
fun showFragment(fragmentManager: FragmentManager, tag: String) {
|
||||
showFragment(fragmentManager, tag, false)
|
||||
}
|
||||
|
||||
fun showFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
tag: String,
|
||||
hideOthers: Boolean,
|
||||
containerId: Int = R.id.fragmentContainer
|
||||
) {
|
||||
val fragments = fragmentManager.fragments
|
||||
|
||||
if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty")
|
||||
|
||||
var fragmentToShow: Fragment? = null
|
||||
|
||||
for (fragment in fragments) {
|
||||
if (fragment.tag != null && fragment.tag == tag) {
|
||||
fragmentToShow = fragment
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
if (fragmentToShow == null) {
|
||||
throw NullPointerException("Required fragment is null")
|
||||
} else {
|
||||
transaction.show(fragmentToShow)
|
||||
}
|
||||
|
||||
if (hideOthers) {
|
||||
for (fragment in fragments) {
|
||||
if (fragment.tag != null && fragment.tag == tag) continue
|
||||
transaction.hide(fragment)
|
||||
}
|
||||
}
|
||||
|
||||
transaction.commit()
|
||||
}
|
||||
|
||||
fun clearFragments(fragmentManager: FragmentManager) {
|
||||
val fragments = fragmentManager.fragments
|
||||
|
||||
if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty")
|
||||
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
for (fragment in fragments) {
|
||||
transaction.remove(fragment)
|
||||
}
|
||||
|
||||
transaction.commitNow()
|
||||
}
|
||||
|
||||
fun hideFragments(fragmentManager: FragmentManager) {
|
||||
val fragments = fragmentManager.fragments
|
||||
|
||||
if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty")
|
||||
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
for (fragment in fragments) {
|
||||
transaction.hide(fragment)
|
||||
}
|
||||
|
||||
transaction.commitNow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import com.meloda.fast.receiver.MinuteReceiver
|
||||
import java.util.*
|
||||
|
||||
object TimeManager {
|
||||
|
||||
var currentHour = 0
|
||||
var currentMinute = 0
|
||||
var currentSecond = 0
|
||||
|
||||
private val onHourChangeListeners: ArrayList<OnHourChangeListener> = ArrayList()
|
||||
private val onMinuteChangeListeners: ArrayList<OnMinuteChangeListener> = ArrayList()
|
||||
private val onSecondChangeListeners: ArrayList<OnSecondChangeListener> = ArrayList()
|
||||
private val onTimeChangeListeners: ArrayList<OnTimeChangeListener> = ArrayList()
|
||||
|
||||
fun init(context: Context) {
|
||||
context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK"))
|
||||
|
||||
addOnMinuteChangeListener(minuteChangeListener)
|
||||
}
|
||||
|
||||
private var minuteChangeListener = object : OnMinuteChangeListener {
|
||||
override fun onMinuteChange(currentMinute: Int) {
|
||||
TimeManager.currentMinute = currentMinute
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
removeOnMinuteChangeListener(minuteChangeListener)
|
||||
}
|
||||
|
||||
fun broadcastMinute() {
|
||||
for (onMinuteChangeListener in onMinuteChangeListeners) {
|
||||
onMinuteChangeListener.onMinuteChange(0)
|
||||
}
|
||||
}
|
||||
|
||||
val isMorning = currentHour in 7..11
|
||||
|
||||
val isAfternoon = currentHour in 12..16
|
||||
|
||||
val isEvening = currentHour in 17..22
|
||||
|
||||
val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1
|
||||
|
||||
fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) {
|
||||
TimeManager.onHourChangeListeners.add(onHourChangeListeners)
|
||||
}
|
||||
|
||||
fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) {
|
||||
onHourChangeListeners.remove(onHourChangeListener)
|
||||
}
|
||||
|
||||
fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) {
|
||||
onMinuteChangeListeners.add(onMinuteChangeListener)
|
||||
}
|
||||
|
||||
fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) {
|
||||
onMinuteChangeListeners.remove(onMinuteChangeListener)
|
||||
}
|
||||
|
||||
fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) {
|
||||
onSecondChangeListeners.add(onSecondChangeListener)
|
||||
}
|
||||
|
||||
fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) {
|
||||
onSecondChangeListeners.remove(onSecondChangeListener)
|
||||
}
|
||||
|
||||
fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) {
|
||||
onTimeChangeListeners.add(onTimeChangeListener)
|
||||
}
|
||||
|
||||
fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
|
||||
onTimeChangeListeners.remove(onTimeChangeListener)
|
||||
}
|
||||
|
||||
interface OnHourChangeListener {
|
||||
fun onHourChange(currentHour: Int)
|
||||
}
|
||||
|
||||
interface OnMinuteChangeListener {
|
||||
fun onMinuteChange(currentMinute: Int)
|
||||
}
|
||||
|
||||
interface OnSecondChangeListener {
|
||||
fun onSecondChange(currentSecond: Int)
|
||||
}
|
||||
|
||||
interface OnTimeChangeListener {
|
||||
fun onHourChange(currentHour: Int)
|
||||
fun onMinuteChange(currentMinute: Int)
|
||||
fun onSecondChange(currentSecond: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import android.util.Log
|
||||
import androidx.collection.arrayMapOf
|
||||
import com.meloda.fast.concurrent.TaskManager
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.model.NewUpdateInfo
|
||||
import com.meloda.fast.net.HttpRequest
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object UpdateManager {
|
||||
|
||||
interface OnUpdateListener {
|
||||
fun onNewUpdate(updateInfo: NewUpdateInfo)
|
||||
|
||||
fun onNoUpdates()
|
||||
}
|
||||
|
||||
private const val checkLink = "https://melodev.procsec.top/vkm/project_vkm_ota.json"
|
||||
|
||||
private const val PRODUCT_NAME = "project_vkm"
|
||||
private const val BRANCH = "alpha"
|
||||
private const val OFFSET = 0
|
||||
|
||||
private const val TAG = "UpdateManager"
|
||||
|
||||
fun checkUpdates(onUpdateListener: OnUpdateListener) {
|
||||
TaskManager.execute {
|
||||
val newLink = "https://temply.procsec.top/prop/deploy/api/method/getOTA"
|
||||
|
||||
val params = arrayMapOf<String, String>()
|
||||
params["product"] = PRODUCT_NAME
|
||||
params["branch"] = BRANCH
|
||||
params["offset"] = OFFSET.toString()
|
||||
params["code"] = AppGlobal.versionCode.toString()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Request started")
|
||||
}
|
||||
|
||||
HttpRequest[newLink, params].asString().let {
|
||||
AppGlobal.post {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "response: $it")
|
||||
}
|
||||
|
||||
val response: Any = if (it == "[]") JSONArray(it) else JSONObject(it)
|
||||
|
||||
val newUpdateInfo: NewUpdateInfo? =
|
||||
if (response is JSONArray) null else NewUpdateInfo(response as JSONObject)
|
||||
|
||||
if (response is JSONArray || newUpdateInfo?.version?.isEmpty() == true || newUpdateInfo?.version == AppGlobal.versionName) {
|
||||
onUpdateListener.onNoUpdates()
|
||||
return@post
|
||||
} else {
|
||||
newUpdateInfo?.let { onUpdateListener.onNewUpdate(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HttpRequest[checkLink].asString().let {
|
||||
// val response = JSONObject(it)
|
||||
//
|
||||
// val updateInfo = UpdateInfo(response)
|
||||
//
|
||||
// AppGlobal.handler.post {
|
||||
// if (updateInfo.version.isEmpty() || updateInfo.version == AppGlobal.versionName) {
|
||||
// onUpdateListener.onNoUpdates()
|
||||
// return@post
|
||||
// }
|
||||
//
|
||||
// if (AppGlobal.versionName != updateInfo.version) {
|
||||
// onUpdateListener.onNewUpdate(updateInfo)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.meloda.fast.concurrent
|
||||
|
||||
class EventInfo<T> constructor(var key: String, var data: T? = null)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.meloda.fast.concurrent
|
||||
|
||||
import android.os.Process
|
||||
|
||||
class LowThread(runnable: Runnable?) : Thread(runnable) {
|
||||
|
||||
override fun run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
|
||||
super.run()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.meloda.fast.concurrent
|
||||
|
||||
object TaskManager {
|
||||
|
||||
private const val TAG = "TaskManager"
|
||||
|
||||
private val listeners = arrayListOf<OnEventListener>()
|
||||
|
||||
fun addOnEventListener(listener: OnEventListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeOnEventListener(listener: OnEventListener?) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun execute(runnable: Runnable?) {
|
||||
LowThread(runnable).start()
|
||||
}
|
||||
|
||||
fun sendEvent(eventInfo: EventInfo<*>) {
|
||||
for (listener in listeners) {
|
||||
listener.onNewEvent(eventInfo)
|
||||
}
|
||||
}
|
||||
|
||||
interface OnEventListener {
|
||||
fun onNewEvent(info: EventInfo<*>)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.meloda.fast.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import com.meloda.fast.common.AppGlobal.Companion.database
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_CHATS
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_FRIENDS
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_MESSAGES
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_USERS
|
||||
import com.meloda.fast.database.storage.ChatsStorage
|
||||
import com.meloda.fast.database.storage.GroupsStorage
|
||||
import com.meloda.fast.database.storage.MessagesStorage
|
||||
import com.meloda.fast.database.storage.UsersStorage
|
||||
import com.meloda.fast.api.model.VKConversation
|
||||
import com.meloda.fast.api.model.VKMessage
|
||||
import com.meloda.fast.api.model.VKUser
|
||||
import java.util.*
|
||||
|
||||
object CacheStorage {
|
||||
|
||||
val usersStorage = UsersStorage()
|
||||
val messagesStorage = MessagesStorage()
|
||||
val chatsStorage = ChatsStorage()
|
||||
val groupsStorage = GroupsStorage()
|
||||
|
||||
fun selectCursor(tableName: String): Cursor {
|
||||
return QueryBuilder.query()
|
||||
.select("*").from(tableName)
|
||||
.asCursor(database)
|
||||
}
|
||||
|
||||
fun selectCursor(tableName: String, where: String): Cursor {
|
||||
return QueryBuilder.query()
|
||||
.select("*").from(tableName)
|
||||
.where(where)
|
||||
.asCursor(database)
|
||||
}
|
||||
|
||||
fun selectCursor(tableName: String, columnName: String, value: Any): Cursor {
|
||||
return QueryBuilder.query()
|
||||
.select("*").from(tableName)
|
||||
.where("$columnName=$value")
|
||||
.asCursor(database)
|
||||
}
|
||||
|
||||
fun selectCursor(tableName: String, columnName: String, ids: IntArray): Cursor {
|
||||
val where = StringBuilder(5 * ids.size)
|
||||
|
||||
where.append("$columnName=${ids[0]}")
|
||||
|
||||
for (i in 1 until ids.size) {
|
||||
where.append(" OR ")
|
||||
where.append("$columnName=${ids[i]}")
|
||||
}
|
||||
|
||||
return selectCursor(tableName, where.toString())
|
||||
}
|
||||
|
||||
fun getInt(cursor: Cursor, columnName: String) =
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(columnName))
|
||||
|
||||
fun getString(cursor: Cursor, columnName: String) =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(columnName))
|
||||
|
||||
fun getBlob(cursor: Cursor, columnName: String) =
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(columnName))
|
||||
|
||||
fun <T> insert(tableName: String, values: ArrayList<T>) {
|
||||
database.beginTransaction()
|
||||
|
||||
val contentValues = ContentValues()
|
||||
|
||||
for (value in values) {
|
||||
when (tableName) {
|
||||
TABLE_USERS -> {
|
||||
usersStorage.cacheValue(contentValues, value as VKUser)
|
||||
break
|
||||
}
|
||||
TABLE_FRIENDS -> {
|
||||
usersStorage.cacheValue(
|
||||
contentValues,
|
||||
value as VKUser,
|
||||
Bundle().apply { putBoolean("toFriends", true) })
|
||||
break
|
||||
}
|
||||
TABLE_MESSAGES -> {
|
||||
messagesStorage.cacheValue(contentValues, value as VKMessage)
|
||||
break
|
||||
}
|
||||
TABLE_CHATS -> {
|
||||
chatsStorage.cacheValue(contentValues, value as VKConversation)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
database.insert(tableName, null, contentValues)
|
||||
contentValues.clear()
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
}
|
||||
|
||||
fun delete(tableName: String, whereClause: String, vararg whereArgs: String) {
|
||||
database.delete(tableName, whereClause, whereArgs)
|
||||
}
|
||||
|
||||
fun delete(tableName: String) {
|
||||
database.delete(tableName, null, null)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.meloda.fast.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
|
||||
class DatabaseHelper constructor(context: Context) : SQLiteOpenHelper(
|
||||
context,
|
||||
DB_NAME,
|
||||
null,
|
||||
DB_VERSION
|
||||
) {
|
||||
companion object {
|
||||
private const val DB_NAME = "cache.db"
|
||||
private const val DB_VERSION = 1
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(DatabaseUtils.createUsersTable())
|
||||
db.execSQL(DatabaseUtils.createGroupsTable())
|
||||
db.execSQL(DatabaseUtils.createFriendsTable())
|
||||
db.execSQL(DatabaseUtils.createMessagesTable())
|
||||
db.execSQL(DatabaseUtils.createChatsTable())
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.meloda.fast.database
|
||||
|
||||
object DatabaseKeys {
|
||||
|
||||
const val ID = "_id"
|
||||
|
||||
const val SORT_ID = "_sort_id"
|
||||
|
||||
const val USER_ID = "_user_id"
|
||||
|
||||
const val FIRST_NAME = "_first_name"
|
||||
|
||||
const val LAST_NAME = "_last_name"
|
||||
|
||||
const val DEACTIVATED = "_deactivated"
|
||||
|
||||
const val GENDER = "_gender"
|
||||
|
||||
const val SCREEN_NAME = "_screen_name"
|
||||
|
||||
const val PHOTOS = "_photos"
|
||||
|
||||
const val IS_ONLINE = "_is_online"
|
||||
|
||||
const val IS_ONLINE_MOBILE = "_is_online_mobile"
|
||||
|
||||
const val STATUS = "_status"
|
||||
|
||||
const val LAST_SEEN = "_last_seen"
|
||||
|
||||
const val MESSAGE_ID = "_message_id"
|
||||
|
||||
const val DATE = "_date"
|
||||
|
||||
const val PEER_ID = "_peer_id"
|
||||
|
||||
const val FROM_ID = "_from_id"
|
||||
|
||||
const val EDIT_TIME = "_edit_time"
|
||||
|
||||
const val IS_OUT = "_is_out"
|
||||
|
||||
const val TEXT = "_text"
|
||||
|
||||
const val RANDOM_ID = "_random_id"
|
||||
|
||||
const val CONVERSATION_MESSAGE_ID = "_conversation_message_id"
|
||||
|
||||
const val ATTACHMENTS = "_attachments"
|
||||
|
||||
const val FWD_MESSAGES = "_fwd_messages"
|
||||
|
||||
const val REPLY_MESSAGE_ID = "_reply_message_id"
|
||||
|
||||
const val ACTION = "_action"
|
||||
|
||||
const val IS_ALLOWED = "_is_allowed"
|
||||
|
||||
const val NOT_ALLOWED_REASON = "_not_allowed_reason"
|
||||
|
||||
const val IN_READ_MESSAGE_ID = "_in_read_message_id"
|
||||
|
||||
const val OUT_READ_MESSAGE_ID = "_out_read_message_id"
|
||||
|
||||
const val LAST_MESSAGE_ID = "_last_message_id"
|
||||
|
||||
const val UNREAD_COUNT = "_unread_count"
|
||||
|
||||
const val CONVERSATION_ID = "_conversation_id"
|
||||
|
||||
const val TYPE = "_type"
|
||||
|
||||
const val LOCAL_ID = "_local_id"
|
||||
|
||||
const val IS_NOTIFICATIONS_DISABLED = "_is_notifications_disabled"
|
||||
|
||||
const val MEMBERS_COUNT = "_members_count"
|
||||
|
||||
const val TITLE = "_title"
|
||||
|
||||
const val PINNED_MESSAGE_ID = "_pinned_message_id"
|
||||
|
||||
const val CHAT_STATE = "_chat_state"
|
||||
|
||||
const val IS_GROUP_CHANNEL = "_is_group_channel"
|
||||
|
||||
const val FRIEND_ID = "_friend_id"
|
||||
|
||||
const val GROUP_ID = "_group_id"
|
||||
|
||||
const val NAME = "_name"
|
||||
|
||||
const val IS_CLOSED = "_is_closed"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.meloda.fast.database
|
||||
|
||||
import com.meloda.fast.database.DatabaseKeys.ACTION
|
||||
import com.meloda.fast.database.DatabaseKeys.ATTACHMENTS
|
||||
import com.meloda.fast.database.DatabaseKeys.CHAT_STATE
|
||||
import com.meloda.fast.database.DatabaseKeys.CONVERSATION_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.CONVERSATION_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.DATE
|
||||
import com.meloda.fast.database.DatabaseKeys.DEACTIVATED
|
||||
import com.meloda.fast.database.DatabaseKeys.EDIT_TIME
|
||||
import com.meloda.fast.database.DatabaseKeys.FIRST_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.FRIEND_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.FROM_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.FWD_MESSAGES
|
||||
import com.meloda.fast.database.DatabaseKeys.GENDER
|
||||
import com.meloda.fast.database.DatabaseKeys.GROUP_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.IN_READ_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ALLOWED
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_CLOSED
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_GROUP_CHANNEL
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_NOTIFICATIONS_DISABLED
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ONLINE
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ONLINE_MOBILE
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_OUT
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_SEEN
|
||||
import com.meloda.fast.database.DatabaseKeys.LOCAL_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.MEMBERS_COUNT
|
||||
import com.meloda.fast.database.DatabaseKeys.MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.NOT_ALLOWED_REASON
|
||||
import com.meloda.fast.database.DatabaseKeys.OUT_READ_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.PEER_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.PHOTOS
|
||||
import com.meloda.fast.database.DatabaseKeys.PINNED_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.RANDOM_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.REPLY_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.SCREEN_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.SORT_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.STATUS
|
||||
import com.meloda.fast.database.DatabaseKeys.TEXT
|
||||
import com.meloda.fast.database.DatabaseKeys.TITLE
|
||||
import com.meloda.fast.database.DatabaseKeys.TYPE
|
||||
import com.meloda.fast.database.DatabaseKeys.UNREAD_COUNT
|
||||
import com.meloda.fast.database.DatabaseKeys.USER_ID
|
||||
|
||||
object DatabaseUtils {
|
||||
|
||||
const val TABLE_USERS = "users"
|
||||
const val TABLE_MESSAGES = "messages"
|
||||
const val TABLE_CHATS = "chats"
|
||||
const val TABLE_FRIENDS = "friends"
|
||||
const val TABLE_GROUPS = "groups"
|
||||
|
||||
private val usersTableMap = HashMap<String, String>().apply {
|
||||
this[USER_ID] = "integer primary key on conflict replace"
|
||||
this[FIRST_NAME] = "varchar(255)"
|
||||
this[LAST_NAME] = "varchar(255)"
|
||||
this[DEACTIVATED] = "varchar(255)"
|
||||
this[GENDER] = "integer default 0"
|
||||
this[SCREEN_NAME] = "varchar(255)"
|
||||
this[PHOTOS] = "text"
|
||||
this[IS_ONLINE] = "integer default 0"
|
||||
this[IS_ONLINE_MOBILE] = "integer default 0"
|
||||
this[STATUS] = "varchar(255)"
|
||||
this[LAST_SEEN] = "integer"
|
||||
}
|
||||
|
||||
private val groupsTableMap = HashMap<String, String>().apply {
|
||||
this[GROUP_ID] = "integer primary key on conflict replace"
|
||||
this[NAME] = "varchar(255)"
|
||||
this[SCREEN_NAME] = "varchar(255)"
|
||||
this[IS_CLOSED] = "integer default 0"
|
||||
this[DEACTIVATED] = "varchar(255)"
|
||||
this[TYPE] = "varchar(255)"
|
||||
this[PHOTOS] = "text"
|
||||
}
|
||||
|
||||
private val messagesTableMap = HashMap<String, String>().apply {
|
||||
this[MESSAGE_ID] = "integer primary key on conflict replace"
|
||||
this[DATE] = "integer"
|
||||
this[PEER_ID] = "integer"
|
||||
this[FROM_ID] = "integer"
|
||||
this[EDIT_TIME] = "integer"
|
||||
this[IS_OUT] = "integer default 0"
|
||||
this[TEXT] = "text"
|
||||
this[RANDOM_ID] = "integer"
|
||||
this[CONVERSATION_MESSAGE_ID] = "integer"
|
||||
this[ATTACHMENTS] = "blob"
|
||||
this[REPLY_MESSAGE_ID] = "integer"
|
||||
this[ACTION] = "blob"
|
||||
|
||||
//2,3,4,5 - message_ids
|
||||
this[FWD_MESSAGES] = "text"
|
||||
}
|
||||
|
||||
private val chatsTableMap = HashMap<String, String>().apply {
|
||||
this[CONVERSATION_ID] = "integer primary key on conflict replace"
|
||||
this[IS_ALLOWED] = "integer default 1"
|
||||
this[NOT_ALLOWED_REASON] = "integer"
|
||||
this[IN_READ_MESSAGE_ID] = "integer"
|
||||
this[OUT_READ_MESSAGE_ID] = "integer"
|
||||
this[LAST_MESSAGE_ID] = "integer"
|
||||
this[UNREAD_COUNT] = "integer"
|
||||
this[LOCAL_ID] = "integer"
|
||||
this[IS_NOTIFICATIONS_DISABLED] = "integer default 0"
|
||||
this[MEMBERS_COUNT] = "integer"
|
||||
this[TITLE] = "varchar(255)"
|
||||
this[IS_GROUP_CHANNEL] = "integer default 0"
|
||||
this[TYPE] = "integer"
|
||||
this[CHAT_STATE] = "integer"
|
||||
this[PHOTOS] = "text"
|
||||
|
||||
this[PINNED_MESSAGE_ID] = "integer"
|
||||
}
|
||||
|
||||
private val friendsTableMap = HashMap<String, String>().apply {
|
||||
this[FRIEND_ID] = "integer primary key on conflict replace"
|
||||
this[SORT_ID] = "integer"
|
||||
|
||||
//id which user friend
|
||||
this[USER_ID] = "integer"
|
||||
}
|
||||
|
||||
fun createUsersTable() = createTableQuery(TABLE_USERS, usersTableMap)
|
||||
fun createGroupsTable() = createTableQuery(TABLE_GROUPS, groupsTableMap)
|
||||
fun createMessagesTable() = createTableQuery(TABLE_MESSAGES, messagesTableMap)
|
||||
fun createChatsTable() = createTableQuery(TABLE_CHATS, chatsTableMap)
|
||||
fun createFriendsTable() = createTableQuery(TABLE_FRIENDS, friendsTableMap)
|
||||
|
||||
private fun createTableQuery(tableName: String, tableData: HashMap<String, String>): String {
|
||||
val builder = StringBuilder("create table $tableName (")
|
||||
|
||||
val entry: Map.Entry<String, String> = tableData.entries.first()
|
||||
builder.append(entry.key)
|
||||
builder.append(" ")
|
||||
builder.append(entry.value)
|
||||
|
||||
tableData.forEach {
|
||||
if (it == entry) return@forEach
|
||||
builder.append(", ")
|
||||
builder.append(it.key)
|
||||
builder.append(" ")
|
||||
builder.append(it.value)
|
||||
}
|
||||
|
||||
builder.append(");")
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.meloda.fast.database
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
|
||||
class QueryBuilder private constructor() {
|
||||
|
||||
companion object {
|
||||
fun query(): QueryBuilder {
|
||||
return QueryBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
private val builder: StringBuilder = StringBuilder()
|
||||
|
||||
fun select(column: String): QueryBuilder {
|
||||
builder.append("SELECT ")
|
||||
.append(column)
|
||||
.append(" ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun from(table: String): QueryBuilder {
|
||||
builder.append("FROM ")
|
||||
.append(table)
|
||||
.append(" ")
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
fun where(clause: String): QueryBuilder {
|
||||
builder.append("WHERE ")
|
||||
.append(clause)
|
||||
.append(" ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun leftJoin(table: String): QueryBuilder {
|
||||
builder.append("LEFT JOIN ")
|
||||
.append(table)
|
||||
.append(" ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun on(where: String): QueryBuilder {
|
||||
builder.append("ON ")
|
||||
.append(where)
|
||||
.append(" ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun and(): QueryBuilder {
|
||||
builder.append("AND ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun or(): QueryBuilder {
|
||||
builder.append("OR ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun asCursor(db: SQLiteDatabase): Cursor {
|
||||
return db.rawQuery(toString(), null)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return builder.toString().trim()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.meloda.fast.database.base
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
|
||||
abstract class Storage<T> {
|
||||
|
||||
abstract val tag: String
|
||||
|
||||
protected var database = AppGlobal.database
|
||||
|
||||
@WorkerThread
|
||||
abstract fun getAllValues(): ArrayList<T>
|
||||
|
||||
@WorkerThread
|
||||
abstract fun insertValues(values: ArrayList<T>, params: Bundle? = null)
|
||||
|
||||
@WorkerThread
|
||||
fun insertValue(value: T, params: Bundle? = null) {
|
||||
insertValues(arrayListOf(value), params)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
abstract fun cacheValue(values: ContentValues, value: T, params: Bundle? = null)
|
||||
|
||||
@WorkerThread
|
||||
abstract fun parseValue(cursor: Cursor): T
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.meloda.fast.database.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.database.CacheStorage
|
||||
import com.meloda.fast.database.CacheStorage.messagesStorage
|
||||
import com.meloda.fast.database.DatabaseKeys.CHAT_STATE
|
||||
import com.meloda.fast.database.DatabaseKeys.CONVERSATION_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.IN_READ_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ALLOWED
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_GROUP_CHANNEL
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_NOTIFICATIONS_DISABLED
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.LOCAL_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.MEMBERS_COUNT
|
||||
import com.meloda.fast.database.DatabaseKeys.NOT_ALLOWED_REASON
|
||||
import com.meloda.fast.database.DatabaseKeys.OUT_READ_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.PHOTOS
|
||||
import com.meloda.fast.database.DatabaseKeys.PINNED_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.TITLE
|
||||
import com.meloda.fast.database.DatabaseKeys.TYPE
|
||||
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 org.json.JSONObject
|
||||
|
||||
@WorkerThread
|
||||
class ChatsStorage : Storage<VKConversation>() {
|
||||
|
||||
override val tag = "ChatsStorage"
|
||||
|
||||
override fun getAllValues(): ArrayList<VKConversation> {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_CHATS)
|
||||
val conversations = ArrayList<VKConversation>()
|
||||
|
||||
while (cursor.moveToNext()) conversations.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
|
||||
return conversations
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun insertValues(values: ArrayList<VKConversation>, params: Bundle?) {
|
||||
if (values.isEmpty()) return
|
||||
|
||||
database.beginTransaction()
|
||||
|
||||
val contentValues = ContentValues()
|
||||
|
||||
for (value in values) {
|
||||
cacheValue(contentValues, value, params)
|
||||
|
||||
database.insert(TABLE_CHATS, null, contentValues)
|
||||
|
||||
contentValues.clear()
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
|
||||
Log.d(tag, "Successful cached chats")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun cacheValue(values: ContentValues, value: VKConversation, params: Bundle?) {
|
||||
values.put(CONVERSATION_ID, value.id)
|
||||
values.put(IS_ALLOWED, value.isAllowed)
|
||||
values.put(NOT_ALLOWED_REASON, value.notAllowedReason.value)
|
||||
values.put(IN_READ_MESSAGE_ID, value.inReadMessageId)
|
||||
values.put(OUT_READ_MESSAGE_ID, value.outReadMessageId)
|
||||
values.put(LAST_MESSAGE_ID, value.lastMessageId)
|
||||
values.put(UNREAD_COUNT, value.unreadCount)
|
||||
values.put(LOCAL_ID, value.localId)
|
||||
values.put(IS_NOTIFICATIONS_DISABLED, value.notificationsEnabled)
|
||||
values.put(MEMBERS_COUNT, value.membersCount)
|
||||
values.put(TITLE, value.title)
|
||||
values.put(IS_GROUP_CHANNEL, value.isGroupChannel)
|
||||
values.put(TYPE, value.intType)
|
||||
values.put(CHAT_STATE, value.intState)
|
||||
|
||||
values.put(
|
||||
PHOTOS,
|
||||
VKUtil.putPhotosToJson(
|
||||
value.photo50,
|
||||
value.photo100,
|
||||
value.photo200
|
||||
).toString()
|
||||
)
|
||||
|
||||
value.pinnedMessage?.let {
|
||||
values.put(PINNED_MESSAGE_ID, it.id)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun parseValue(cursor: Cursor): VKConversation {
|
||||
val conversation = VKConversation()
|
||||
|
||||
conversation.id = CacheStorage.getInt(cursor, CONVERSATION_ID)
|
||||
conversation.isAllowed = CacheStorage.getInt(cursor, IS_ALLOWED) == 1
|
||||
conversation.notAllowedReason = VKConversation.Reason.fromInt(
|
||||
CacheStorage.getInt(cursor, NOT_ALLOWED_REASON)
|
||||
)
|
||||
conversation.inReadMessageId = CacheStorage.getInt(cursor, IN_READ_MESSAGE_ID)
|
||||
conversation.outReadMessageId = CacheStorage.getInt(cursor, OUT_READ_MESSAGE_ID)
|
||||
conversation.unreadCount = CacheStorage.getInt(cursor, UNREAD_COUNT)
|
||||
conversation.localId = CacheStorage.getInt(cursor, LOCAL_ID)
|
||||
conversation.notificationsEnabled =
|
||||
CacheStorage.getInt(cursor, IS_NOTIFICATIONS_DISABLED) == 1
|
||||
conversation.membersCount = CacheStorage.getInt(cursor, MEMBERS_COUNT)
|
||||
conversation.title = CacheStorage.getString(cursor, TITLE)
|
||||
conversation.isGroupChannel = CacheStorage.getInt(cursor, IS_GROUP_CHANNEL) == 1
|
||||
|
||||
val pinnedMessageId = CacheStorage.getInt(cursor, PINNED_MESSAGE_ID)
|
||||
if (pinnedMessageId != -1) {
|
||||
val pinnedMessage = messagesStorage.getMessageById(pinnedMessageId)
|
||||
if (pinnedMessage != null) conversation.pinnedMessage = pinnedMessage
|
||||
}
|
||||
|
||||
conversation.intType = CacheStorage.getInt(cursor, TYPE)
|
||||
conversation.intState = CacheStorage.getInt(cursor, CHAT_STATE)
|
||||
|
||||
conversation.lastMessageId = CacheStorage.getInt(cursor, LAST_MESSAGE_ID)
|
||||
val lastMessage = messagesStorage.getMessageById(conversation.lastMessageId)
|
||||
if (lastMessage != null) conversation.lastMessage = lastMessage
|
||||
|
||||
val photos = VKUtil.parseJsonPhotos(JSONObject(CacheStorage.getString(cursor, PHOTOS)))
|
||||
conversation.photo50 = photos[0]
|
||||
conversation.photo100 = photos[1]
|
||||
conversation.photo200 = photos[2]
|
||||
|
||||
return conversation
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.meloda.fast.database.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.database.CacheStorage
|
||||
import com.meloda.fast.database.CacheStorage.getInt
|
||||
import com.meloda.fast.database.CacheStorage.getString
|
||||
import com.meloda.fast.database.DatabaseKeys.DEACTIVATED
|
||||
import com.meloda.fast.database.DatabaseKeys.GROUP_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_CLOSED
|
||||
import com.meloda.fast.database.DatabaseKeys.NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.PHOTOS
|
||||
import com.meloda.fast.database.DatabaseKeys.SCREEN_NAME
|
||||
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 org.json.JSONObject
|
||||
|
||||
class GroupsStorage : Storage<VKGroup>() {
|
||||
|
||||
override val tag = "GroupsStorage"
|
||||
|
||||
@WorkerThread
|
||||
fun getGroups(ids: IntArray): ArrayList<VKGroup> {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_GROUPS, GROUP_ID, ids)
|
||||
|
||||
val groups = ArrayList<VKGroup>(cursor.count)
|
||||
while (cursor.moveToNext()) groups.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
return groups
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getGroup(userId: Int): VKGroup? {
|
||||
val group = getGroups(intArrayOf(userId))
|
||||
|
||||
return if (group.isNotEmpty()) group[0] else null
|
||||
}
|
||||
|
||||
override fun getAllValues(): ArrayList<VKGroup> {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_GROUPS)
|
||||
val groups = ArrayList<VKGroup>()
|
||||
|
||||
while (cursor.moveToNext()) groups.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
override fun insertValues(values: ArrayList<VKGroup>, params: Bundle?) {
|
||||
if (values.isEmpty()) return
|
||||
|
||||
database.beginTransaction()
|
||||
|
||||
val contentValues = ContentValues()
|
||||
|
||||
for (value in values) {
|
||||
cacheValue(contentValues, value, params)
|
||||
|
||||
database.insert(TABLE_GROUPS, null, contentValues)
|
||||
|
||||
contentValues.clear()
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
|
||||
Log.d(tag, "Successful cached groups")
|
||||
}
|
||||
|
||||
override fun cacheValue(values: ContentValues, value: VKGroup, params: Bundle?) {
|
||||
values.put(GROUP_ID, value.id)
|
||||
values.put(NAME, value.name)
|
||||
values.put(SCREEN_NAME, value.screenName)
|
||||
values.put(IS_CLOSED, value.isClosed)
|
||||
values.put(DEACTIVATED, value.deactivated)
|
||||
values.put(TYPE, value.type.value)
|
||||
|
||||
val photos =
|
||||
VKUtil.putPhotosToJson(value.photo50, value.photo100, value.photo200).toString()
|
||||
|
||||
values.put(PHOTOS, photos)
|
||||
}
|
||||
|
||||
override fun parseValue(cursor: Cursor): VKGroup {
|
||||
val group = VKGroup()
|
||||
|
||||
group.id = getInt(cursor, GROUP_ID)
|
||||
group.name = getString(cursor, NAME)
|
||||
group.screenName = getString(cursor, SCREEN_NAME)
|
||||
group.isClosed = getInt(cursor, IS_CLOSED) == 1
|
||||
group.deactivated = getString(cursor, DEACTIVATED)
|
||||
group.type = VKGroup.Type.fromString(getString(cursor, TYPE))
|
||||
|
||||
val photos = VKUtil.parseJsonPhotos(JSONObject(getString(cursor, PHOTOS)))
|
||||
|
||||
group.photo50 = photos[0]
|
||||
group.photo100 = photos[1]
|
||||
group.photo200 = photos[2]
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.meloda.fast.database.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.database.CacheStorage
|
||||
import com.meloda.fast.database.CacheStorage.selectCursor
|
||||
import com.meloda.fast.database.DatabaseKeys.ACTION
|
||||
import com.meloda.fast.database.DatabaseKeys.ATTACHMENTS
|
||||
import com.meloda.fast.database.DatabaseKeys.CONVERSATION_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.DATE
|
||||
import com.meloda.fast.database.DatabaseKeys.EDIT_TIME
|
||||
import com.meloda.fast.database.DatabaseKeys.FROM_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.FWD_MESSAGES
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_OUT
|
||||
import com.meloda.fast.database.DatabaseKeys.MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.PEER_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.RANDOM_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.REPLY_MESSAGE_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.TEXT
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_MESSAGES
|
||||
import com.meloda.fast.database.base.Storage
|
||||
import com.meloda.fast.util.Utils
|
||||
import com.meloda.fast.api.model.VKMessage
|
||||
import com.meloda.fast.api.model.VKMessageAction
|
||||
import com.meloda.fast.api.model.VKModel
|
||||
import java.util.stream.Collectors
|
||||
|
||||
@WorkerThread
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MessagesStorage : Storage<VKMessage>() {
|
||||
|
||||
override val tag = "MessagesStorage"
|
||||
|
||||
@WorkerThread
|
||||
fun getMessagesHistory(peerId: Int): ArrayList<VKMessage> {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_MESSAGES, PEER_ID, peerId)
|
||||
|
||||
val messages = ArrayList<VKMessage>(cursor.count)
|
||||
while (cursor.moveToNext()) messages.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getMessageById(messageId: Int): VKMessage? {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_MESSAGES, MESSAGE_ID, messageId)
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
val message = parseValue(cursor)
|
||||
cursor.close()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getAllValues(): ArrayList<VKMessage> {
|
||||
val cursor = selectCursor(TABLE_MESSAGES)
|
||||
val messages = ArrayList<VKMessage>()
|
||||
|
||||
while (cursor.moveToNext()) messages.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun insertValues(values: ArrayList<VKMessage>, params: Bundle?) {
|
||||
if (values.isEmpty()) return
|
||||
|
||||
database.beginTransaction()
|
||||
|
||||
val contentValues = ContentValues()
|
||||
|
||||
for (value in values) {
|
||||
cacheValue(contentValues, value)
|
||||
|
||||
database.insert(TABLE_MESSAGES, null, contentValues)
|
||||
|
||||
contentValues.clear()
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
|
||||
Log.d(tag, "Successful cached messages")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun cacheValue(values: ContentValues, value: VKMessage, params: Bundle?) {
|
||||
values.put(MESSAGE_ID, value.id)
|
||||
values.put(DATE, value.date)
|
||||
values.put(PEER_ID, value.peerId)
|
||||
values.put(FROM_ID, value.fromId)
|
||||
values.put(EDIT_TIME, value.editTime)
|
||||
values.put(TEXT, value.text)
|
||||
values.put(RANDOM_ID, value.randomId)
|
||||
values.put(CONVERSATION_MESSAGE_ID, value.conversationMessageId)
|
||||
|
||||
value.replyMessage?.let {
|
||||
values.put(REPLY_MESSAGE_ID, it.id)
|
||||
}
|
||||
|
||||
value.action?.let {
|
||||
values.put(ACTION, Utils.serialize(it))
|
||||
}
|
||||
|
||||
value.attachments.let {
|
||||
if (it.isNotEmpty()) {
|
||||
values.put(ATTACHMENTS, Utils.serialize(it))
|
||||
}
|
||||
}
|
||||
|
||||
value.fwdMessages.let {
|
||||
if (it.isNotEmpty()) {
|
||||
val ids = arrayListOf<String>()
|
||||
it.forEach { message -> ids.add(message.id.toString()) }
|
||||
|
||||
ids.stream().collect(Collectors.joining(",")).let { str ->
|
||||
values.put(FWD_MESSAGES, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun parseValue(cursor: Cursor): VKMessage {
|
||||
val message = VKMessage()
|
||||
|
||||
message.id = CacheStorage.getInt(cursor, MESSAGE_ID)
|
||||
message.date = CacheStorage.getInt(cursor, DATE)
|
||||
message.peerId = CacheStorage.getInt(cursor, PEER_ID)
|
||||
message.fromId = CacheStorage.getInt(cursor, FROM_ID)
|
||||
message.editTime = CacheStorage.getInt(cursor, EDIT_TIME)
|
||||
message.isOut = CacheStorage.getInt(cursor, IS_OUT) == 1
|
||||
message.text = CacheStorage.getString(cursor, TEXT)
|
||||
message.randomId = CacheStorage.getInt(cursor, RANDOM_ID)
|
||||
message.conversationMessageId = CacheStorage.getInt(cursor, CONVERSATION_MESSAGE_ID)
|
||||
|
||||
val blobAttachments = Utils.deserialize(CacheStorage.getBlob(cursor, ATTACHMENTS))
|
||||
if (blobAttachments != null) message.attachments = blobAttachments as ArrayList<VKModel>
|
||||
else message.attachments = arrayListOf()
|
||||
|
||||
val replyMessageId = CacheStorage.getInt(cursor, REPLY_MESSAGE_ID)
|
||||
val replyMessage = getMessageById(replyMessageId)
|
||||
if (replyMessage != null) message.replyMessage = replyMessage
|
||||
|
||||
val blobAction = Utils.deserialize(CacheStorage.getBlob(cursor, ACTION))
|
||||
if (blobAction != null) message.action = blobAction as VKMessageAction
|
||||
|
||||
val stringFwdMessages = CacheStorage.getString(cursor, FWD_MESSAGES)
|
||||
if (stringFwdMessages != null) {
|
||||
val split = stringFwdMessages.split(',')
|
||||
|
||||
val ids = arrayListOf<Int>()
|
||||
for (s in split) ids.add(s.toInt())
|
||||
|
||||
val fwdMessages = arrayListOf<VKMessage>()
|
||||
|
||||
ids.forEach {
|
||||
val fwdMessage = getMessageById(it)
|
||||
if (fwdMessage != null) fwdMessages.add(fwdMessage)
|
||||
}
|
||||
|
||||
message.fwdMessages = fwdMessages
|
||||
} else message.fwdMessages = arrayListOf()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.meloda.fast.database.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.meloda.fast.UserConfig
|
||||
import com.meloda.fast.database.CacheStorage
|
||||
import com.meloda.fast.database.CacheStorage.selectCursor
|
||||
import com.meloda.fast.database.DatabaseKeys.DEACTIVATED
|
||||
import com.meloda.fast.database.DatabaseKeys.FIRST_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.FRIEND_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.GENDER
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ONLINE
|
||||
import com.meloda.fast.database.DatabaseKeys.IS_ONLINE_MOBILE
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.LAST_SEEN
|
||||
import com.meloda.fast.database.DatabaseKeys.PHOTOS
|
||||
import com.meloda.fast.database.DatabaseKeys.SCREEN_NAME
|
||||
import com.meloda.fast.database.DatabaseKeys.SORT_ID
|
||||
import com.meloda.fast.database.DatabaseKeys.STATUS
|
||||
import com.meloda.fast.database.DatabaseKeys.USER_ID
|
||||
import com.meloda.fast.database.DatabaseUtils.TABLE_FRIENDS
|
||||
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 org.json.JSONObject
|
||||
|
||||
@WorkerThread
|
||||
class UsersStorage : Storage<VKUser>() {
|
||||
|
||||
override val tag = "UsersStorage"
|
||||
|
||||
@WorkerThread
|
||||
fun getUsers(ids: IntArray): ArrayList<VKUser> {
|
||||
val cursor = CacheStorage.selectCursor(TABLE_USERS, USER_ID, ids)
|
||||
|
||||
val users = ArrayList<VKUser>(cursor.count)
|
||||
while (cursor.moveToNext()) users.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
return users
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getUser(userId: Int): VKUser? {
|
||||
val user = getUsers(intArrayOf(userId))
|
||||
|
||||
return if (user.isNotEmpty()) user[0] else null
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getFriends(userId: Int, onlyOnline: Boolean = false): ArrayList<VKUser> {
|
||||
val cursor = QueryBuilder.query()
|
||||
.select("*")
|
||||
.from(TABLE_FRIENDS)
|
||||
.leftJoin(TABLE_USERS)
|
||||
.on("friends.${FRIEND_ID} = users.$USER_ID")
|
||||
.where("friends.${USER_ID} = $userId")
|
||||
.asCursor(database)
|
||||
|
||||
val users = ArrayList<VKUser>(cursor.count)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val userOnline = CacheStorage.getInt(cursor, IS_ONLINE) == 1
|
||||
if (onlyOnline && !userOnline) continue
|
||||
|
||||
val user = parseValue(cursor)
|
||||
users.add(user)
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
override fun getAllValues(): ArrayList<VKUser> {
|
||||
val cursor = selectCursor(TABLE_USERS)
|
||||
val users = ArrayList<VKUser>()
|
||||
|
||||
while (cursor.moveToNext()) users.add(parseValue(cursor))
|
||||
|
||||
cursor.close()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun insertValues(values: ArrayList<VKUser>, params: Bundle?) {
|
||||
if (values.isEmpty()) return
|
||||
|
||||
val toFriends = params?.getBoolean("toFriends") ?: false
|
||||
|
||||
database.beginTransaction()
|
||||
|
||||
val contentValues = ContentValues()
|
||||
|
||||
for (user in values) {
|
||||
cacheValue(contentValues, user, params)
|
||||
|
||||
database.insert(if (toFriends) TABLE_FRIENDS else TABLE_USERS, null, contentValues)
|
||||
|
||||
contentValues.clear()
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
|
||||
Log.d(tag, "Successful cached users. toFriends: $toFriends")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun cacheValue(values: ContentValues, value: VKUser, params: Bundle?) {
|
||||
val toFriends = params?.getBoolean("toFriends") ?: false
|
||||
|
||||
if (toFriends) {
|
||||
values.put(USER_ID, UserConfig.userId)
|
||||
values.put(FRIEND_ID, value.userId)
|
||||
values.put(SORT_ID, value.sortId)
|
||||
return
|
||||
}
|
||||
|
||||
values.put(USER_ID, value.userId)
|
||||
values.put(FIRST_NAME, value.firstName)
|
||||
values.put(LAST_NAME, value.lastName)
|
||||
values.put(DEACTIVATED, value.deactivated)
|
||||
values.put(GENDER, value.sex)
|
||||
values.put(SCREEN_NAME, value.screenName)
|
||||
values.put(IS_ONLINE, value.isOnline)
|
||||
values.put(IS_ONLINE_MOBILE, value.isOnlineMobile)
|
||||
values.put(STATUS, value.status)
|
||||
values.put(LAST_SEEN, value.lastSeen)
|
||||
|
||||
values.put(
|
||||
PHOTOS,
|
||||
VKUtil.putPhotosToJson(
|
||||
value.photo50,
|
||||
value.photo100,
|
||||
value.photo200
|
||||
).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun parseValue(cursor: Cursor): VKUser {
|
||||
val user = VKUser()
|
||||
|
||||
user.userId = CacheStorage.getInt(cursor, USER_ID)
|
||||
user.firstName = CacheStorage.getString(cursor, FIRST_NAME)
|
||||
user.lastName = CacheStorage.getString(cursor, LAST_NAME)
|
||||
user.deactivated = CacheStorage.getString(cursor, DEACTIVATED)
|
||||
user.sex = CacheStorage.getInt(cursor, GENDER)
|
||||
user.screenName = CacheStorage.getString(cursor, SCREEN_NAME)
|
||||
user.isOnline = CacheStorage.getInt(cursor, IS_ONLINE) == 1
|
||||
user.isOnlineMobile = CacheStorage.getInt(cursor, IS_ONLINE_MOBILE) == 1
|
||||
user.status = CacheStorage.getString(cursor, STATUS)
|
||||
user.lastSeen = CacheStorage.getInt(cursor, LAST_SEEN)
|
||||
|
||||
val photos =
|
||||
VKUtil.parseJsonPhotos(JSONObject(CacheStorage.getString(cursor, PHOTOS)))
|
||||
|
||||
user.photo50 = photos[0]
|
||||
user.photo100 = photos[1]
|
||||
user.photo200 = photos[2]
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
|
||||
object ContextExtensions {
|
||||
|
||||
fun Context.drawable(@DrawableRes resId: Int): Drawable? {
|
||||
return ContextCompat.getDrawable(this, resId)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.color(@ColorRes resId: Int): Int {
|
||||
return ContextCompat.getColor(this, resId)
|
||||
}
|
||||
|
||||
fun Context.font(@FontRes resId: Int): Typeface? {
|
||||
return ResourcesCompat.getFont(this, resId)
|
||||
}
|
||||
|
||||
fun Context.string(@StringRes resId: Int): String {
|
||||
return getString(resId)
|
||||
}
|
||||
|
||||
fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View {
|
||||
return LayoutInflater.from(this).inflate(resId, root, attachToRoot)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
object DrawableExtensions {
|
||||
|
||||
fun Drawable?.tint(@ColorInt color: Int): Drawable? {
|
||||
this?.setTint(color)
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object FloatExtensions {
|
||||
|
||||
fun Float.int(): Int {
|
||||
return roundToInt()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
object LiveDataExtensions {
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.set(position: Int, v: T) {
|
||||
val value = (this.value ?: arrayListOf()).apply { this[position] = v }
|
||||
this.value = value
|
||||
}
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.get(position: Int): T {
|
||||
return (value as MutableList<T>)[position]
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun <T> MutableLiveData<MutableList<T>>.add(v: T, position: Int = -1) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
if (position == -1) this.add(v) else this.add(position, v)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun <T> MutableLiveData<MutableList<T>>.addAll(values: List<T>, position: Int = -1) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
if (position == -1) this.addAll(values)
|
||||
else this.addAll(position, values)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
@Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING")
|
||||
fun <T> MutableLiveData<MutableList<T>>.removeAll(values: List<T>) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.removeAll(values)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.removeAt(index: Int) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.removeAt(index)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.remove(item: T) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.remove(item)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.iterator(): Iterator<T> {
|
||||
return (value as MutableList<T>).iterator()
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.clear() {
|
||||
value = arrayListOf()
|
||||
}
|
||||
|
||||
val <T> MutableLiveData<MutableList<T>>.indices get() = (value as MutableList<T>).indices
|
||||
|
||||
val <T> MutableLiveData<MutableList<T>>.size get() = (value as MutableList<T>).size
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.isEmpty(): Boolean {
|
||||
return (value as MutableList<T>).isEmpty()
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.isNotEmpty(): Boolean {
|
||||
return !isEmpty()
|
||||
}
|
||||
|
||||
fun <T> MutableLiveData<MutableList<T>>.requireValue() = value!!
|
||||
|
||||
@UiThread
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(values: List<T>) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.addAll(values)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(v: T) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.add(v)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.minusAssign(values: List<T>) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.removeAll(values)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
operator fun <T> MutableLiveData<MutableList<T>>.minusAssign(v: T) {
|
||||
val value = (this.value ?: arrayListOf()).apply {
|
||||
this.remove(v)
|
||||
}
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.SparseArray
|
||||
import androidx.core.util.forEach
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.UserConfig
|
||||
|
||||
/**
|
||||
* Manages the various graphs needed for a [BottomNavigationView].
|
||||
*
|
||||
* This sample is a workaround until the Navigation Component supports multiple back stacks.
|
||||
*/
|
||||
object NavigationExtensions {
|
||||
|
||||
fun BottomNavigationView.setupWithNavController(
|
||||
navGraphIds: List<Int>,
|
||||
fragmentManager: FragmentManager,
|
||||
containerId: Int,
|
||||
intent: Intent
|
||||
): LiveData<NavController> {
|
||||
|
||||
// Map of tags
|
||||
val graphIdToTagMap = SparseArray<String>()
|
||||
// Result. Mutable live data with the selected controlled
|
||||
val selectedNavController = MutableLiveData<NavController>()
|
||||
|
||||
var firstFragmentGraphId = 0
|
||||
|
||||
// First create a NavHostFragment for each NavGraph ID
|
||||
navGraphIds.forEachIndexed { index, navGraphId ->
|
||||
val fragmentTag = getFragmentTag(index)
|
||||
|
||||
// Find or create the Navigation host fragment
|
||||
val navHostFragment = obtainNavHostFragment(
|
||||
fragmentManager,
|
||||
fragmentTag,
|
||||
navGraphId,
|
||||
containerId
|
||||
)
|
||||
|
||||
// Obtain its id
|
||||
val graphId = navHostFragment.navController.graph.id
|
||||
|
||||
if (index == 0) {
|
||||
firstFragmentGraphId = graphId
|
||||
}
|
||||
|
||||
// Save to the map
|
||||
graphIdToTagMap[graphId] = fragmentTag
|
||||
|
||||
// Attach or detach nav host fragment depending on whether it's the selected item.
|
||||
if (this.selectedItemId == graphId) {
|
||||
// Update livedata with the selected graph
|
||||
selectedNavController.value = navHostFragment.navController
|
||||
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
|
||||
} else {
|
||||
detachNavHostFragment(fragmentManager, navHostFragment)
|
||||
}
|
||||
}
|
||||
|
||||
// Now connect selecting an item with swapping Fragments
|
||||
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
|
||||
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
|
||||
var isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
|
||||
setOnItemSelectedListener { item ->
|
||||
// Don't do anything if the state is state has already been saved.
|
||||
if (fragmentManager.isStateSaved) {
|
||||
false
|
||||
} else {
|
||||
val navController =
|
||||
(fragmentManager.findFragmentByTag(selectedItemTag) as NavHostFragment).navController
|
||||
navController.popBackStack(navController.graph.startDestination, false)
|
||||
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
|
||||
val newlySelectedItemTag = //graphIdToTagMap[item.itemId]
|
||||
if (!UserConfig.isLoggedIn()) graphIdToTagMap[R.id.login] else graphIdToTagMap[item.itemId]
|
||||
|
||||
fragmentManager.popBackStack(
|
||||
firstFragmentTag,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE
|
||||
)
|
||||
val selectedFragment =
|
||||
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
|
||||
|
||||
// Exclude the first fragment tag because it's always in the back stack.
|
||||
if (firstFragmentTag != newlySelectedItemTag) {
|
||||
// Commit a transaction that cleans the back stack and adds the first fragment
|
||||
// to it, creating the fixed started destination.
|
||||
fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(
|
||||
R.anim.nav_default_enter_anim,
|
||||
R.anim.nav_default_exit_anim,
|
||||
R.anim.nav_default_pop_enter_anim,
|
||||
R.anim.nav_default_pop_exit_anim
|
||||
)
|
||||
.attach(selectedFragment)
|
||||
.setPrimaryNavigationFragment(selectedFragment)
|
||||
.apply {
|
||||
// Detach all other Fragments
|
||||
graphIdToTagMap.forEach { _, fragmentTagIter ->
|
||||
if (fragmentTagIter != newlySelectedItemTag) {
|
||||
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
.addToBackStack(firstFragmentTag)
|
||||
.setReorderingAllowed(true)
|
||||
.commit()
|
||||
}
|
||||
selectedItemTag = newlySelectedItemTag
|
||||
isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
selectedNavController.value = selectedFragment.navController
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOnItemReselectedListener { item ->
|
||||
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
|
||||
val selectedFragment =
|
||||
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
|
||||
val navController = selectedFragment.navController
|
||||
// Pop the back stack to the start destination of the current navController graph
|
||||
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
|
||||
fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(
|
||||
R.anim.nav_default_enter_anim,
|
||||
R.anim.nav_default_exit_anim,
|
||||
R.anim.nav_default_pop_enter_anim,
|
||||
R.anim.nav_default_pop_exit_anim
|
||||
)
|
||||
.attach(selectedFragment)
|
||||
.setPrimaryNavigationFragment(selectedFragment)
|
||||
.apply {
|
||||
// Detach all other Fragments
|
||||
graphIdToTagMap.forEach { _, fragmentTagIter ->
|
||||
if (fragmentTagIter != newlySelectedItemTag) {
|
||||
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
.addToBackStack(firstFragmentTag)
|
||||
.setReorderingAllowed(true)
|
||||
.commit()
|
||||
selectedItemTag = newlySelectedItemTag
|
||||
isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
selectedNavController.value = selectedFragment.navController
|
||||
} else navController.popBackStack(navController.graph.startDestination, false)
|
||||
}
|
||||
// Optional: on item reselected, pop back stack to the destination of the graph
|
||||
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
|
||||
|
||||
// Finally, ensure that we update our BottomNavigationView when the back stack changes
|
||||
fragmentManager.addOnBackStackChangedListener {
|
||||
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
|
||||
this.selectedItemId = firstFragmentGraphId
|
||||
}
|
||||
|
||||
// Reset the graph if the currentDestination is not valid (happens when the back
|
||||
// stack is popped after using the back button).
|
||||
selectedNavController.value?.let { controller ->
|
||||
if (controller.currentDestination == null) {
|
||||
controller.navigate(controller.graph.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedNavController
|
||||
}
|
||||
|
||||
private fun BottomNavigationView.setupDeepLinks(
|
||||
navGraphIds: List<Int>,
|
||||
fragmentManager: FragmentManager,
|
||||
containerId: Int,
|
||||
intent: Intent
|
||||
) {
|
||||
navGraphIds.forEachIndexed { index, navGraphId ->
|
||||
val fragmentTag = getFragmentTag(index)
|
||||
|
||||
// Find or create the Navigation host fragment
|
||||
val navHostFragment = obtainNavHostFragment(
|
||||
fragmentManager,
|
||||
fragmentTag,
|
||||
navGraphId,
|
||||
containerId
|
||||
)
|
||||
// Handle Intent
|
||||
if (navHostFragment.navController.handleDeepLink(intent) &&
|
||||
selectedItemId != navHostFragment.navController.graph.id
|
||||
) {
|
||||
this.selectedItemId = navHostFragment.navController.graph.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun detachNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
navHostFragment: NavHostFragment
|
||||
) {
|
||||
fragmentManager.beginTransaction()
|
||||
.detach(navHostFragment)
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
private fun attachNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
navHostFragment: NavHostFragment,
|
||||
isPrimaryNavFragment: Boolean
|
||||
) {
|
||||
fragmentManager.beginTransaction()
|
||||
.attach(navHostFragment)
|
||||
.apply {
|
||||
if (isPrimaryNavFragment) {
|
||||
setPrimaryNavigationFragment(navHostFragment)
|
||||
}
|
||||
}
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
private fun obtainNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
fragmentTag: String,
|
||||
navGraphId: Int,
|
||||
containerId: Int,
|
||||
): NavHostFragment {
|
||||
// If the Nav Host fragment exists, return it
|
||||
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
|
||||
existingFragment?.let { return it }
|
||||
|
||||
// Otherwise, create it and return it.
|
||||
val navHostFragment = NavHostFragment.create(navGraphId)
|
||||
fragmentManager.beginTransaction()
|
||||
.add(containerId, navHostFragment, fragmentTag)
|
||||
.commitNow()
|
||||
return navHostFragment
|
||||
}
|
||||
|
||||
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
|
||||
val backStackCount = backStackEntryCount
|
||||
for (index in 0 until backStackCount) {
|
||||
if (getBackStackEntryAt(index).name == backStackName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val FragmentManager.visibleFragments
|
||||
get(): List<Fragment> {
|
||||
val visibleFragments = arrayListOf<Fragment>()
|
||||
fragments.forEach { if (it.isVisible) visibleFragments.add(it) }
|
||||
return visibleFragments
|
||||
}
|
||||
|
||||
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import java.util.*
|
||||
|
||||
object StringExtensions {
|
||||
|
||||
fun String.lowerCase(): String {
|
||||
return toLowerCase(Locale.getDefault())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
object TextViewExtensions {
|
||||
|
||||
fun TextView.clear() {
|
||||
text = ""
|
||||
}
|
||||
|
||||
fun TextInputLayout.clear() {
|
||||
editText?.setText("")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.meloda.fast.io
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class BytesOutputStream : ByteArrayOutputStream {
|
||||
constructor() : super(8192)
|
||||
constructor(size: Int) : super(size)
|
||||
|
||||
val byteArray: ByteArray = buf
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.meloda.fast.io
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object Charsets {
|
||||
|
||||
val ASCII: Charset = StandardCharsets.US_ASCII
|
||||
|
||||
val UTF_8: Charset = StandardCharsets.UTF_8
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.meloda.fast.io
|
||||
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlin.math.max
|
||||
|
||||
object EasyStreams {
|
||||
|
||||
const val BUFFER_SIZE = 8192
|
||||
const val CHAR_BUFFER_SIZE = 4096
|
||||
|
||||
@JvmOverloads
|
||||
@Throws(IOException::class)
|
||||
fun read(from: InputStream, encoding: Charset? = Charsets.UTF_8): String {
|
||||
return read(InputStreamReader(from, encoding))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun read(from: Reader): String {
|
||||
val builder = StringWriter(CHAR_BUFFER_SIZE)
|
||||
return try {
|
||||
copy(from, builder)
|
||||
builder.toString()
|
||||
} finally {
|
||||
close(from)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun readBytes(from: InputStream): ByteArray {
|
||||
val output = ByteArrayOutputStream(max(from.available(), BUFFER_SIZE))
|
||||
try {
|
||||
copy(from, output)
|
||||
} finally {
|
||||
close(from)
|
||||
}
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(from: ByteArray?, to: OutputStream) {
|
||||
try {
|
||||
to.write(from)
|
||||
to.flush()
|
||||
} finally {
|
||||
close(to)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(from: String?, to: OutputStream?) {
|
||||
write(from, OutputStreamWriter(to, Charsets.UTF_8))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(from: CharArray?, to: Writer) {
|
||||
try {
|
||||
to.write(from)
|
||||
to.flush()
|
||||
} finally {
|
||||
close(to)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun write(from: String?, to: Writer) {
|
||||
try {
|
||||
to.write(from)
|
||||
to.flush()
|
||||
} finally {
|
||||
close(to)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copy(from: Reader, to: Writer): Long {
|
||||
val buffer = CharArray(CHAR_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var total: Long = 0
|
||||
while (from.read(buffer).also { read = it } != -1) {
|
||||
to.write(buffer, 0, read)
|
||||
total += read.toLong()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copy(from: InputStream, to: OutputStream): Long {
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var read: Int
|
||||
var total: Long = 0
|
||||
while (from.read(buffer).also { read = it } != -1) {
|
||||
to.write(buffer, 0, read)
|
||||
total += read.toLong()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fun buffer(input: InputStream?): BufferedInputStream {
|
||||
return buffer(input, BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
fun buffer(input: InputStream?, size: Int): BufferedInputStream {
|
||||
return if (input is BufferedInputStream) input else BufferedInputStream(input, size)
|
||||
}
|
||||
|
||||
fun buffer(output: OutputStream?): BufferedOutputStream {
|
||||
return buffer(output, BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
fun buffer(output: OutputStream?, size: Int): BufferedOutputStream {
|
||||
return if (output is BufferedOutputStream) output else BufferedOutputStream(output, size)
|
||||
}
|
||||
|
||||
fun buffer(input: Reader?): BufferedReader {
|
||||
return buffer(input, CHAR_BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
fun buffer(input: Reader?, size: Int): BufferedReader {
|
||||
return if (input is BufferedReader) input else BufferedReader(input, size)
|
||||
}
|
||||
|
||||
fun buffer(output: Writer?): BufferedWriter {
|
||||
return buffer(output, CHAR_BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
fun buffer(output: Writer?, size: Int): BufferedWriter {
|
||||
return if (output is BufferedWriter) output else BufferedWriter(output, size)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun gzip(input: InputStream?): GZIPInputStream {
|
||||
return gzip(input, BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
@Throws(IOException::class)
|
||||
fun gzip(input: InputStream?, size: Int): GZIPInputStream {
|
||||
return if (input is GZIPInputStream) input else GZIPInputStream(input, size)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun gzip(input: OutputStream?): GZIPOutputStream {
|
||||
return gzip(input, BUFFER_SIZE)
|
||||
}
|
||||
|
||||
@Contract("null, _ -> new")
|
||||
@Throws(IOException::class)
|
||||
fun gzip(input: OutputStream?, size: Int): GZIPOutputStream {
|
||||
return if (input is GZIPOutputStream) input else GZIPOutputStream(input, size)
|
||||
}
|
||||
|
||||
fun close(c: Closeable?): Boolean {
|
||||
if (c != null) {
|
||||
try {
|
||||
c.close()
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.meloda.fast.io
|
||||
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.io.*
|
||||
import java.math.BigInteger
|
||||
|
||||
object FileStreams {
|
||||
|
||||
val lineSeparatorChar = lineSeparator()[0]
|
||||
|
||||
const val ONE_KB = 1024
|
||||
const val ONE_MB = ONE_KB * 1024
|
||||
const val ONE_GB = ONE_MB * 1024
|
||||
const val ONE_TB = ONE_GB * 1024L
|
||||
const val ONE_PB = ONE_TB * 1024L
|
||||
const val ONE_EB = ONE_PB * 1024L
|
||||
|
||||
val ONE_ZB: BigInteger = BigInteger.valueOf(ONE_EB).multiply(BigInteger.valueOf(1024L))
|
||||
val ONE_YB: BigInteger = ONE_ZB.multiply(BigInteger.valueOf(1024L))
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun read(from: File?): String {
|
||||
return EasyStreams.read(reader(from))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(from: String?, to: File?) {
|
||||
EasyStreams.write(from, writer(to))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(from: ByteArray?, to: File?) {
|
||||
EasyStreams.write(from, FileOutputStream(to))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun append(from: ByteArray?, to: File?) {
|
||||
EasyStreams.write(from, FileOutputStream(to, true))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun append(from: CharArray?, to: File?) {
|
||||
EasyStreams.write(from, FileWriter(to, true))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun append(from: CharSequence, to: File?) {
|
||||
EasyStreams.write(if (from is String) from else from.toString(), FileWriter(to, true))
|
||||
}
|
||||
|
||||
fun delete(dir: File) {
|
||||
if (dir.isDirectory) {
|
||||
val files = dir.listFiles() ?: return
|
||||
for (file in files) {
|
||||
delete(file)
|
||||
}
|
||||
} else {
|
||||
dir.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun lineSeparator(): String {
|
||||
return System.lineSeparator()
|
||||
}
|
||||
|
||||
fun search(dir: File, name: String?): File? {
|
||||
require(dir.isDirectory) { "dir can't be file." }
|
||||
|
||||
val files = dir.listFiles() ?: return null
|
||||
|
||||
if (files.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (file in files) {
|
||||
if (file.isDirectory) {
|
||||
search(file, name)
|
||||
} else if (file.name.contains(name!!)) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Contract("_ -> new")
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun reader(from: File?): Reader {
|
||||
return InputStreamReader(FileInputStream(from), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@Contract("_ -> new")
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun writer(to: File?): Writer {
|
||||
return OutputStreamWriter(FileOutputStream(to), Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.meloda.fast.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class NewUpdateInfo() {
|
||||
|
||||
var id: Int = 0
|
||||
var version: String = ""
|
||||
var code: Int = 0
|
||||
var time: Int = 0
|
||||
var changelog: String = ""
|
||||
|
||||
// var branchId: Int = 0
|
||||
// var branchName: String = ""
|
||||
|
||||
var downloadLink: String = ""
|
||||
|
||||
// var state: Boolean = true
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
id = o.optInt("id")
|
||||
version = o.optString("version")
|
||||
code = o.optInt("code")
|
||||
time = o.optInt("time")
|
||||
changelog = o.optString("changelog")
|
||||
|
||||
downloadLink = o.optString("download")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.meloda.fast.model
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
class UpdateInfo() {
|
||||
|
||||
var version: String = ""
|
||||
var code: Int = 0
|
||||
var changelog: String = ""
|
||||
var downloadLink: String = ""
|
||||
var date: Int = 0
|
||||
|
||||
constructor(o: JSONObject) : this() {
|
||||
version = o.optString("lastVersionName")
|
||||
code = o.optInt("lastVersionCode")
|
||||
changelog = o.optString("changelog")
|
||||
downloadLink = o.optString("downloadLink")
|
||||
date = o.optInt("buildDate")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.meloda.fast.net
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
|
||||
class HttpRequest(
|
||||
private val url: String,
|
||||
private val method: String,
|
||||
private val params: ArrayMap<String, String> = ArrayMap<String, String>()
|
||||
) {
|
||||
companion object {
|
||||
const val GET = "GET"
|
||||
const val POST = "POST"
|
||||
|
||||
operator fun get(
|
||||
url: String,
|
||||
params: ArrayMap<String, String> = ArrayMap()
|
||||
): HttpRequest {
|
||||
return HttpRequest(url, GET, params)
|
||||
}
|
||||
|
||||
operator fun get(url: String): HttpRequest {
|
||||
return Companion[url, ArrayMap()]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var connection: HttpURLConnection? = null
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun asString(): String {
|
||||
val input = getStream()
|
||||
|
||||
val content = com.meloda.fast.io.EasyStreams.read(input)
|
||||
|
||||
connection?.disconnect()
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun asBytes(): ByteArray {
|
||||
val input = getStream()
|
||||
val content = com.meloda.fast.io.EasyStreams.readBytes(input)
|
||||
|
||||
connection?.disconnect()
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getStream(): InputStream {
|
||||
if (connection == null) {
|
||||
connection = createConnection()
|
||||
}
|
||||
|
||||
var input = connection!!.inputStream
|
||||
val encoding = connection!!.getHeaderField("Content-Encoding")
|
||||
if ("gzip".equals(encoding, ignoreCase = true)) {
|
||||
input = com.meloda.fast.io.EasyStreams.gzip(input)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
private fun getParams(): String {
|
||||
val buffer = StringBuilder()
|
||||
|
||||
for (i in 0 until params.size) {
|
||||
val key = params.keyAt(i)
|
||||
val value = params.valueAt(i)
|
||||
buffer.append(key).append("=")
|
||||
buffer.append(URLEncoder.encode(value, "UTF-8"))
|
||||
buffer.append("&")
|
||||
}
|
||||
return buffer.toString()
|
||||
}
|
||||
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
private fun getUrl(): String {
|
||||
return if (params.isNotEmpty() && "GET".equals(method, ignoreCase = true)) {
|
||||
url + "?" + getParams()
|
||||
} else url
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createConnection(): HttpURLConnection? {
|
||||
connection = URL(getUrl()).openConnection() as HttpURLConnection
|
||||
connection!!.readTimeout = 60000
|
||||
connection!!.connectTimeout = 60000
|
||||
connection!!.useCaches = true
|
||||
connection!!.doInput = true
|
||||
connection!!.doOutput =
|
||||
!GET.equals(method, ignoreCase = true)
|
||||
connection!!.requestMethod = method
|
||||
connection!!.setRequestProperty("Accept-Encoding", "gzip")
|
||||
return connection
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
try {
|
||||
return asString()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.fast.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.meloda.fast.api.OnResponseListener
|
||||
|
||||
open class DownloadUpdateReceiver : BroadcastReceiver() {
|
||||
|
||||
var listener: OnResponseListener<Any?>? = null
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
listener?.onResponse(null)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.fast.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.meloda.fast.common.TimeManager
|
||||
|
||||
class MinuteReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
TimeManager.broadcastMinute()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.meloda.fast.screens.friends
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.base.BaseFragment
|
||||
import com.meloda.fast.databinding.FragmentFriendsBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FriendsFragment : BaseFragment(R.layout.fragment_friends) {
|
||||
|
||||
private val binding: FragmentFriendsBinding by viewBinding()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.meloda.fast.screens.important
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.base.BaseFragment
|
||||
import com.meloda.fast.databinding.FragmentImportantBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImportantFragment : BaseFragment(R.layout.fragment_important) {
|
||||
|
||||
private val binding: FragmentImportantBinding by viewBinding()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
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
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
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.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.FragmentLoginBinding
|
||||
import com.meloda.fast.screens.main.MainFragment
|
||||
import com.meloda.fast.util.KeyboardUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
|
||||
|
||||
override val viewModel: LoginVM by viewModels()
|
||||
private val binding: FragmentLoginBinding by viewBinding()
|
||||
|
||||
private var lastEmail: String = ""
|
||||
private var lastPassword: String = ""
|
||||
|
||||
private var errorTimer: Timer? = null
|
||||
private var captchaInputLayout: TextInputLayout? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
(parentFragment?.parentFragment as? MainFragment)?.bottomBar?.isVisible = false
|
||||
|
||||
prepareViews()
|
||||
|
||||
binding.loginInput.clearFocus()
|
||||
|
||||
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) {
|
||||
super.onEvent(event)
|
||||
|
||||
when (event) {
|
||||
is ShowError -> showErrorSnackbar(event.errorDescription)
|
||||
is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid)
|
||||
is GoToValidationEvent -> goToValidation(event.redirectUrl)
|
||||
is GoToMainEvent -> goToMain(event.haveAuthorized)
|
||||
StartProgressEvent -> onProgressStarted()
|
||||
StopProgressEvent -> onProgressStopped()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgressStarted() {
|
||||
binding.loginContainer.isVisible = false
|
||||
binding.passwordContainer.isVisible = false
|
||||
binding.auth.isVisible = false
|
||||
binding.progress.isVisible = true
|
||||
}
|
||||
|
||||
private fun onProgressStopped() {
|
||||
binding.loginContainer.isVisible = true
|
||||
binding.passwordContainer.isVisible = true
|
||||
binding.auth.isVisible = true
|
||||
binding.progress.isVisible = false
|
||||
}
|
||||
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun preparePasswordEditText() {
|
||||
binding.passwordInput.typeface = Typeface.DEFAULT
|
||||
binding.passwordLayout.endIconMode = TextInputLayout.END_ICON_NONE
|
||||
|
||||
binding.passwordInput.addTextChangedListener {
|
||||
if (!binding.passwordLayout.error.isNullOrBlank()) binding.passwordLayout.error = ""
|
||||
}
|
||||
|
||||
binding.passwordInput.setOnFocusChangeListener { _, hasFocus ->
|
||||
binding.passwordLayout.endIconMode =
|
||||
if (hasFocus) TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
else TextInputLayout.END_ICON_NONE
|
||||
}
|
||||
|
||||
binding.passwordInput.setOnEditorActionListener { _, _, event ->
|
||||
if (event == null) return@setOnEditorActionListener false
|
||||
return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_GO ||
|
||||
(event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER))
|
||||
) {
|
||||
KeyboardUtils.hideKeyboardFrom(binding.passwordInput)
|
||||
binding.auth.performClick()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 7/27/2021 extract strings to resources
|
||||
private fun validateInputData(
|
||||
loginString: String?,
|
||||
passwordString: String?,
|
||||
captchaCode: String? = null
|
||||
): Boolean {
|
||||
var isValidated = true
|
||||
|
||||
if (loginString?.isEmpty() == true) {
|
||||
isValidated = false
|
||||
setError("Input login", binding.loginLayout)
|
||||
}
|
||||
|
||||
if (passwordString?.isEmpty() == true) {
|
||||
isValidated = false
|
||||
setError("Input password", binding.passwordLayout)
|
||||
}
|
||||
|
||||
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
|
||||
isValidated = false
|
||||
setError("Input code", captchaInputLayout!!)
|
||||
}
|
||||
|
||||
return isValidated
|
||||
}
|
||||
|
||||
private fun setError(error: String, inputLayout: TextInputLayout) {
|
||||
inputLayout.error = error
|
||||
|
||||
if (errorTimer != null) {
|
||||
errorTimer?.cancel()
|
||||
errorTimer = null
|
||||
}
|
||||
|
||||
if (errorTimer == null) {
|
||||
errorTimer = Timer()
|
||||
}
|
||||
|
||||
errorTimer?.schedule(2500) {
|
||||
lifecycleScope.launch(Dispatchers.Main) { clearErrors() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearErrors() {
|
||||
binding.loginLayout.error = ""
|
||||
binding.passwordLayout.error = ""
|
||||
|
||||
captchaInputLayout?.error = ""
|
||||
}
|
||||
|
||||
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) {
|
||||
val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false)
|
||||
captchaInputLayout = captchaBinding.captchaLayout
|
||||
|
||||
captchaBinding.image.load(captchaImage) {
|
||||
crossfade(100)
|
||||
transformations(RoundedCornersTransformation(4f))
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
.setView(captchaBinding.root)
|
||||
.setCancelable(false)
|
||||
.setTitle(R.string.input_captcha)
|
||||
|
||||
val dialog = builder.show()
|
||||
|
||||
captchaBinding.ok.setOnClickListener {
|
||||
val captchaCode = captchaBinding.captchaInput.text.toString().trim()
|
||||
|
||||
if (!validateInputData(
|
||||
loginString = null,
|
||||
passwordString = null,
|
||||
captchaCode = captchaCode
|
||||
)
|
||||
) return@setOnClickListener
|
||||
|
||||
dialog.dismiss()
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.login(
|
||||
webView = binding.webView,
|
||||
email = lastEmail,
|
||||
password = lastPassword,
|
||||
captchaSid = captchaSid,
|
||||
captchaKey = captchaCode
|
||||
)
|
||||
}
|
||||
}
|
||||
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
|
||||
private fun showErrorSnackbar(errorDescription: String) {
|
||||
val snackbar = Snackbar.make(
|
||||
requireView(),
|
||||
getString(R.string.error, errorDescription),
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
|
||||
snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE
|
||||
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)
|
||||
|
||||
findNavController().navigate(R.id.toMain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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,75 @@
|
||||
package com.meloda.fast.screens.login
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.VKAuth
|
||||
import com.meloda.fast.base.BaseFragment
|
||||
import com.meloda.fast.databinding.FragmentValidationBinding
|
||||
|
||||
class ValidationFragment : BaseFragment(R.layout.fragment_validation) {
|
||||
|
||||
private val binding: FragmentValidationBinding by viewBinding()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val redirectUrl = getRedirectUrl()
|
||||
|
||||
binding.webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
Log.d("Fast::Validation", "onPageStarted: url: $url")
|
||||
parseUrl(url ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
binding.webView.settings.domStorageEnabled = true
|
||||
binding.webView.clearCache(true)
|
||||
binding.webView.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
val manager = CookieManager.getInstance()
|
||||
manager.removeAllCookies(null)
|
||||
manager.flush()
|
||||
manager.setAcceptCookie(true)
|
||||
|
||||
binding.webView.loadUrl(redirectUrl)
|
||||
}
|
||||
|
||||
private fun getRedirectUrl() = requireArguments().getString("redirectUrl", "")
|
||||
|
||||
private fun parseUrl(url: String) {
|
||||
if (url.startsWith("https://oauth.vk.com/blank.html#success=1")) {
|
||||
if (!url.contains("error=")) {
|
||||
val data = VKAuth.parseRedirectUrl(url)
|
||||
|
||||
val accessToken = data.first
|
||||
val userId = data.second
|
||||
|
||||
parentFragmentManager.setFragmentResult(
|
||||
"validation",
|
||||
bundleOf(
|
||||
"accessToken" to accessToken,
|
||||
"userId" to userId
|
||||
)
|
||||
)
|
||||
|
||||
findNavController().navigate(R.id.toLogin)
|
||||
}
|
||||
} else {
|
||||
Log.d("Fast::Validation", "parseUrl: $url")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.meloda.fast.screens.main
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.UserConfig
|
||||
import com.meloda.fast.base.BaseVMFragment
|
||||
import com.meloda.fast.databinding.FragmentMainBinding
|
||||
import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainFragment : BaseVMFragment<MainVM>(R.layout.fragment_main) {
|
||||
|
||||
override val viewModel: MainVM by viewModels()
|
||||
private val binding: FragmentMainBinding by viewBinding()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) setupBottomBar()
|
||||
|
||||
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
|
||||
}
|
||||
|
||||
private fun setupBottomBar() {
|
||||
val navGraphIds = listOf(
|
||||
R.navigation.messages,
|
||||
R.navigation.friends,
|
||||
R.navigation.important,
|
||||
R.navigation.login
|
||||
)
|
||||
|
||||
with(binding.bottomBar) {
|
||||
selectedItemId = R.id.messages
|
||||
setupWithNavController(
|
||||
navGraphIds = navGraphIds,
|
||||
fragmentManager = childFragmentManager,
|
||||
containerId = R.id.fragmentContainer,
|
||||
intent = requireActivity().intent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val bottomBar get() = binding.bottomBar
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user