diff --git a/.gitignore b/.gitignore
index dcdc776b..aa724b77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,83 +1,15 @@
-# Built application files
-*.apk
-*.ap_
-*.aab
-
-# Files for the ART/Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
-bin/
-gen/
-out/
-# Uncomment the following line in case you need and you don't have the release build type files in your app
-# release/
-
-# Gradle files
-.gradle/
-build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-*.log
-
-# Android Studio Navigation editor temp files
-.navigation/
-
-# Android Studio captures folder
-captures/
-
-# IntelliJ
*.iml
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/gradle.xml
-.idea/assetWizardSettings.xml
-.idea/dictionaries
-.idea/libraries
-# Android Studio 3 in .gitignore file.
-.idea/caches
-.idea/modules.xml
-# Comment next line if keeping position of elements in Navigation Editor is relevant for you
-.idea/navEditor.xml
-
-# Keystore files
-# Uncomment the following lines if you do not want to check your keystore files in.
-#*.jks
-#*.keystore
-
-# External native build folder generated in Android Studio 2.2 and later
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
.externalNativeBuild
-
-# Google Services (e.g. APIs or Firebase)
-# google-services.json
-
-# Freeline
-freeline.py
-freeline/
-freeline_project_description.json
-
-# fastlane
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots
-fastlane/test_output
-fastlane/readme.md
-
-# Version control
-vcs.xml
-
-# lint
-lint/intermediates/
-lint/generated/
-lint/outputs/
-lint/tmp/
-# lint/reports/
+.cxx
+local.properties
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..0691aa87
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,72 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "ru.melod1n.project.vkm"
+ minSdkVersion 23
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ }
+}
+
+dependencies {
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
+
+ implementation 'androidx.core:core-ktx:1.5.0-beta01'
+
+ implementation 'androidx.appcompat:appcompat:1.3.0-beta01'
+ implementation 'androidx.preference:preference-ktx:1.1.1'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
+ implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.fragment:fragment-ktx:1.3.0'
+ implementation 'com.google.android.material:material:1.3.0'
+
+ implementation 'androidx.room:room-runtime:2.3.0-beta01'
+ kapt 'androidx.room:room-compiler:2.3.0-beta01'
+
+ implementation 'com.facebook.fresco:fresco:2.3.0'
+
+ implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
+
+ implementation 'com.squareup.picasso:picasso:2.71828'
+
+ implementation 'com.github.rahatarmanahmed:circularprogressview:2.5.0'
+
+ implementation 'com.google.code.gson:gson:2.8.6'
+
+ implementation 'org.jsoup:jsoup:1.13.1'
+
+ implementation 'ch.acra:acra:4.11.1'
+
+ def appCenterSdkVersion = '4.1.0'
+ implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
+ implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
+ implementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..b53e3372
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 00000000..3370f64a
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt
new file mode 100644
index 00000000..d9052672
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt
@@ -0,0 +1,23 @@
+package ru.melod1n.project.vkm.activity
+
+import android.content.Intent
+import android.os.Bundle
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.api.UserConfig
+
+class DropUserDataActivity : BaseActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ UserConfig.clear()
+
+ TaskManager.execute { AppGlobal.database.clearAllTables() }
+
+ startActivity(Intent(this, MainActivity::class.java))
+ finishAffinity()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt
new file mode 100644
index 00000000..9905716b
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt
@@ -0,0 +1,134 @@
+package ru.melod1n.project.vkm.activity
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.util.Log
+import android.view.MenuItem
+import android.webkit.*
+import android.widget.ProgressBar
+import androidx.core.view.isVisible
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKAuth
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable
+import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint
+import ru.melod1n.project.vkm.widget.Toolbar
+
+class LoginActivity : BaseActivity() {
+
+ private lateinit var toolbar: Toolbar
+ private lateinit var progressBar: ProgressBar
+ private lateinit var webView: WebView
+ private lateinit var refreshLayout: SwipeRefreshLayout
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_login)
+
+ initViews()
+
+ prepareToolbar()
+ prepareRefreshLayout()
+
+ prepareSettings()
+
+ val url = VKAuth.getUrl(UserConfig.API_ID, VKAuth.settings)
+
+ webView.loadUrl(url)
+ }
+
+ private fun initViews() {
+ toolbar = findViewById(R.id.toolbar)
+ progressBar = findViewById(R.id.progressBar)
+ webView = findViewById(R.id.webView)
+ refreshLayout = findViewById(R.id.refreshLayout)
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun prepareSettings() {
+ webView.settings.javaScriptEnabled = true
+ webView.clearCache(true)
+ webView.webViewClient = VKWebClient()
+
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.setAcceptCookie(true)
+ }
+
+ private fun prepareToolbar() {
+ setSupportActionBar(toolbar)
+
+ toolbar.navigationIcon = drawable(R.drawable.ic_close).tint(color(R.color.accent))
+ toolbar.setNavigationClickListener { onBackPressed() }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) onBackPressed()
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun prepareRefreshLayout() {
+ refreshLayout.apply {
+ setColorSchemeColors(color(R.color.accent))
+ setOnRefreshListener {
+ webView.reload()
+ isRefreshing = false
+ }
+ }
+ }
+
+ private inner class VKWebClient : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+ view.loadUrl(url)
+ return true
+ }
+
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+
+ progressBar.isVisible = true
+ view.isVisible = false
+
+ parseUrl(url)
+ }
+
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+
+ progressBar.isVisible = false
+ view.isVisible = true
+ }
+
+ override fun onReceivedError(
+ view: WebView,
+ request: WebResourceRequest?,
+ error: WebResourceError?
+ ) {
+ super.onReceivedError(view, request, error)
+ Log.e("VKM WebView", error.toString())
+ }
+ }
+
+ private fun parseUrl(url: String) {
+ try {
+ if (url.startsWith(VKAuth.redirectUrl) && !url.contains("error=")) {
+ val auth = VKAuth.parseRedirectUrl(url)
+ val token = auth[0]
+ val id = auth[1].toInt()
+
+ UserConfig.token = token
+ UserConfig.userId = id
+ UserConfig.save()
+
+ finishAffinity()
+ startActivity(Intent(this, MainActivity::class.java))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt
new file mode 100644
index 00000000..24752f82
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt
@@ -0,0 +1,260 @@
+package ru.melod1n.project.vkm.activity
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.core.view.GravityCompat
+import androidx.core.view.isVisible
+import androidx.drawerlayout.widget.DrawerLayout
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.navigation.NavigationView
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.FragmentSwitcher
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.common.TimeManager
+import ru.melod1n.project.vkm.dialog.AccountDialog
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable
+import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint
+import ru.melod1n.project.vkm.fragment.FragmentConversations
+import ru.melod1n.project.vkm.fragment.FragmentFriends
+import ru.melod1n.project.vkm.fragment.FragmentSettings
+import ru.melod1n.project.vkm.fragment.LoginFragment
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.service.LongPollService
+import ru.melod1n.project.vkm.util.AndroidUtils
+import ru.melod1n.project.vkm.util.ViewUtils
+import ru.melod1n.project.vkm.widget.Toolbar
+
+
+class MainActivity : BaseActivity(),
+ NavigationView.OnNavigationItemSelectedListener,
+ BottomNavigationView.OnNavigationItemSelectedListener {
+
+ private lateinit var fragmentConversations: FragmentConversations
+ private lateinit var fragmentFriends: FragmentFriends
+ private lateinit var fragmentSettings: FragmentSettings
+
+ private var selectedId = 0
+
+ private lateinit var drawerLayout: DrawerLayout
+ lateinit var bottomBar: BottomNavigationView
+ private lateinit var navigationView: NavigationView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ initViews()
+
+// checkLogin()
+
+ if (UserConfig.isLoggedIn()) {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.fragmentContainer, FragmentConversations())
+ .commit()
+ } else {
+ bottomBar.isVisible = false
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.fragmentContainer, LoginFragment())
+ .commit()
+ }
+
+
+// TimeManager.init(this)
+
+// prepareFragments()
+
+// prepareNavigationView()
+// prepareBottomBar()
+// checkLogin()
+ }
+
+ private fun initViews() {
+ drawerLayout = findViewById(R.id.drawerLayout)
+ bottomBar = findViewById(R.id.bottomBar)
+ navigationView = findViewById(R.id.navigationView)
+ }
+
+ override fun onDestroy() {
+ TimeManager.destroy()
+ super.onDestroy()
+ }
+
+ private fun prepareFragments() {
+ fragmentConversations = FragmentConversations()
+ fragmentFriends = FragmentFriends(UserConfig.userId)
+ fragmentSettings = FragmentSettings()
+
+ val containerId = R.id.fragmentContainer
+
+ FragmentSwitcher.addFragments(
+ supportFragmentManager,
+ containerId,
+ listOf(fragmentConversations)
+ )
+ }
+
+ fun initToolbar(toolbar: Toolbar) {
+ toolbar.navigationIcon =
+ drawable(R.drawable.ic_search).tint(color(R.color.text_secondary_60_alpha))
+
+ toolbar.setTitleMode(Toolbar.TitleMode.HINT)
+ toolbar.setTitle(R.string.action_search)
+ toolbar.setAvatarClickListener { openAccountDialog() }
+ }
+
+ private fun openAccountDialog() {
+ AccountDialog().show(supportFragmentManager, AccountDialog.TAG)
+ }
+
+ private fun prepareNavigationView() {
+ navigationView.layoutParams?.width = AppGlobal.screenWidth - AppGlobal.screenWidth / 6
+
+ navigationView.setNavigationItemSelectedListener(this)
+
+ drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+ }
+
+ private fun prepareBottomBar() {
+// val menu = bottomBar.menu
+//
+// val navigationFriends = menu.add(R.string.navigation_friends)
+// navigationFriends.icon = drawable(R.drawable.ic_people_outline)
+//
+// val navigationConversations = menu.add(R.string.navigation_conversations)
+// navigationConversations.icon = drawable(R.drawable.ic_message_outline)
+//
+// val navigationImportant = menu.add(R.string.navigation_important)
+// navigationImportant.icon = drawable(R.drawable.ic_star_border)
+
+ bottomBar.setOnNavigationItemSelectedListener(this)
+ }
+
+ private fun createMenuItem(menu: Menu, tag: String): MenuItem {
+ return when (tag) {
+ "friends" ->
+ menu.add("Friends").apply { icon = drawable(R.drawable.ic_people_outline) }
+ "conversations" ->
+ menu.add("Conversations").apply { icon = drawable(R.drawable.ic_message_outline) }
+ "important" ->
+ menu.add("Important").apply { icon = drawable(R.drawable.ic_star_border) }
+
+ else -> menu.add("")
+ }
+ }
+
+ private fun checkLogin() {
+ if (UserConfig.isLoggedIn()) {
+ startLongPoll()
+ loadProfileInfo()
+ } else {
+ openStartScreen()
+ }
+ }
+
+ private fun openMainScreen() {
+ selectedId = R.id.navigationConversations
+ bottomBar.selectedItemId = selectedId
+ openConversationsScreen()
+ }
+
+ private fun startLongPoll() {
+ startService(Intent(this, LongPollService::class.java))
+ }
+
+ private fun openStartScreen() {
+ finish()
+ startActivity(Intent(this, StartActivity::class.java))
+ }
+
+ private fun openConversationsScreen() {
+ FragmentSwitcher.showFragment(
+ supportFragmentManager,
+ fragmentConversations.javaClass.simpleName,
+ true
+ )
+ }
+
+ private fun openFriendsScreen() {
+ FragmentSwitcher.showFragment(
+ supportFragmentManager,
+ fragmentFriends.javaClass.simpleName,
+ true
+ )
+ }
+
+ private fun openSettingsScreen() {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ }
+
+ private fun loadProfileInfo() {
+ if (AndroidUtils.hasConnection()) {
+ TaskManager.loadUser(VKApiKeys.UPDATE_USER, UserConfig.userId,
+ object : OnResponseListener {
+ override fun onResponse(response: VKUser) {
+ prepareNavigationHeader(response)
+ openMainScreen()
+ }
+
+ override fun onError(t: Throwable) {
+ openMainScreen()
+ }
+ })
+ }
+ }
+
+ private fun prepareNavigationHeader(user: VKUser) {
+ ViewUtils.prepareNavigationHeader(navigationView.getHeaderView(0), user)
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ switchFragment(item.itemId)
+ return true
+ }
+
+ private fun switchFragment(itemId: Int) {
+ var valid = true
+
+ when (itemId) {
+ R.id.navigationConversations -> {
+ openConversationsScreen()
+ }
+ R.id.navigationFriends -> {
+ openFriendsScreen()
+ }
+ R.id.navigationSettings -> {
+ openSettingsScreen()
+ }
+ else -> {
+ valid = false
+ }
+ }
+
+ if (!valid) return
+
+ if (selectedId != itemId) {
+ selectedId = itemId
+ navigationView.setCheckedItem(selectedId)
+ }
+
+ if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ drawerLayout.closeDrawer(GravityCompat.START)
+ }
+ }
+
+ override fun onBackPressed() {
+ if (drawerLayout.isDrawerOpen(navigationView)) {
+ drawerLayout.closeDrawer(navigationView)
+ } else {
+ super.onBackPressed()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt
new file mode 100644
index 00000000..1b6ef652
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt
@@ -0,0 +1,403 @@
+package ru.melod1n.project.vkm.activity
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.amulyakhare.textdrawable.TextDrawable
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.activity.ui.presenter.MessagesPresenter
+import ru.melod1n.project.vkm.activity.ui.view.MessagesView
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.api.model.VKGroup
+import ru.melod1n.project.vkm.api.model.VKModel
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.dialog.ProfileDialog
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint
+import ru.melod1n.project.vkm.extensions.ImageViewExtensions.loadImage
+import ru.melod1n.project.vkm.fragment.FragmentSettings
+import ru.melod1n.project.vkm.util.KeyboardUtils
+import ru.melod1n.project.vkm.util.TextUtils
+import ru.melod1n.project.vkm.util.ViewUtils
+import ru.melod1n.project.vkm.widget.CircleImageView
+
+
+class MessagesActivity : BaseActivity(), MessagesView {
+
+ companion object {
+ const val TAG = "MessagesActivity"
+
+ const val MESSAGES_COUNT = 30
+
+ const val TAG_EXTRA_TITLE = "title"
+ const val TAG_EXTRA_AVATAR = "avatar"
+ const val TAG_EXTRA_ID = "id"
+ const val TAG_EXTRA_USER = "user"
+ const val TAG_EXTRA_GROUP = "group"
+ }
+
+ private var isEdit = false
+
+ private var fabState = FabState.VOICE
+
+ private enum class FabState {
+ VOICE, SEND, EDIT, DELETE, BLOCKED
+ }
+
+ private var title = ""
+ private var avatar = ""
+
+ private var lastMessageText = ""
+ private var attachments = arrayListOf()
+
+ private var peerId = 0
+
+ private var dialogUser: VKUser? = null
+ private var dialogGroup: VKGroup? = null
+
+ private lateinit var presenter: MessagesPresenter
+
+ lateinit var recyclerView: RecyclerView
+ private lateinit var refreshLayout: SwipeRefreshLayout
+ private lateinit var toolbar: Toolbar
+ private lateinit var chatAvatar: CircleImageView
+ private lateinit var chatTitle: TextView
+ private lateinit var chatInfo: TextView
+ private lateinit var chatPanel: LinearLayout
+ private lateinit var chatMessage: EditText
+ private lateinit var chatSend: ImageButton
+ private lateinit var progressBar: ProgressBar
+
+ private lateinit var noItemsView: LinearLayout
+ private lateinit var noInternetView: LinearLayout
+ private lateinit var errorView: LinearLayout
+
+ override fun onDestroy() {
+ super.onDestroy()
+ presenter.destroy()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_messages)
+ initViews()
+
+ initExtraData()
+
+ prepareToolbar()
+ prepareRefreshLayout()
+ prepareRecyclerView()
+ prepareEditText()
+
+ presenter = MessagesPresenter(this)
+ presenter.setup(peerId, recyclerView)
+ }
+
+ private fun initViews() {
+ toolbar = findViewById(R.id.toolbar)
+ recyclerView = findViewById(R.id.recyclerView)
+ refreshLayout = findViewById(R.id.refreshLayout)
+ chatAvatar = findViewById(R.id.chatAvatar)
+ chatTitle = findViewById(R.id.chatTitle)
+ chatInfo = findViewById(R.id.chatInfo)
+ chatPanel = findViewById(R.id.chatPanel)
+ chatMessage = findViewById(R.id.chatMessage)
+ chatSend = findViewById(R.id.chatSend)
+ progressBar = findViewById(R.id.progressBar)
+
+ noItemsView = findViewById(R.id.noItemsView)
+ noInternetView = findViewById(R.id.noInternetView)
+ errorView = findViewById(R.id.errorView)
+ }
+
+ private fun initExtraData() {
+ peerId = intent.getIntExtra(TAG_EXTRA_ID, -1)
+ title = intent.getStringExtra(TAG_EXTRA_TITLE) ?: ""
+ avatar = intent.getStringExtra(TAG_EXTRA_AVATAR) ?: ""
+
+ dialogUser = intent.getSerializableExtra(TAG_EXTRA_USER) as VKUser?
+ dialogGroup = intent.getSerializableExtra(TAG_EXTRA_GROUP) as VKGroup?
+ }
+
+ private fun prepareToolbar() {
+ setSupportActionBar(toolbar)
+
+ val placeholder = TextDrawable
+ .builder()
+ .buildRound(TextUtils.getFirstLetterFromString(title), color(R.color.accent))
+
+ chatAvatar.setImageDrawable(placeholder)
+
+ chatAvatar.loadImage(avatar, placeholder)
+
+ toolbar.setOnClickListener { presenter.openProfile() }
+
+ chatAvatar.setOnClickListener { presenter.openProfile() }
+
+ chatTitle.text = title
+
+ supportActionBar?.setDisplayShowTitleEnabled(false)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ toolbar.navigationIcon.tint(color(R.color.accent))
+ }
+
+ private fun prepareRefreshLayout() {
+ refreshLayout.isEnabled = false
+ }
+
+ private fun prepareRecyclerView() {
+ recyclerView.layoutManager =
+ LinearLayoutManager(this, RecyclerView.VERTICAL, false).also {
+ it.stackFromEnd = true
+ }
+
+ recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ if (dy < 0 && AppGlobal.inputMethodManager.isAcceptingText && AppGlobal.preferences.getBoolean(
+ FragmentSettings.KEY_HIDE_KEYBOARD_ON_SCROLL_UP,
+ true
+ )
+ ) {
+ KeyboardUtils.hideKeyboardFrom(chatMessage)
+ }
+ }
+ })
+ }
+
+ private fun prepareEditText() {
+ chatMessage.addTextChangedListener {
+ fabState = if (it.toString().trim().isEmpty()) {
+ if (isEdit) {
+ FabState.DELETE
+ } else {
+ FabState.VOICE
+ }
+ } else {
+ if (isEdit) {
+ FabState.EDIT
+ } else {
+ FabState.SEND
+ }
+ }
+
+ refreshFabStyle()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.activity_messages, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> onBackPressed()
+
+ R.id.messagesRefresh -> {
+ presenter.updateData()
+ }
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun refreshFabStyle() {
+ chatSend.isClickable = true
+
+ when (fabState) {
+ FabState.VOICE -> {
+ chatSend.apply {
+ setImageResource(R.drawable.ic_mic)
+
+ setOnClickListener {
+ showVoiceRecordingTip()
+ }
+ setOnLongClickListener {
+ true
+ }
+ }
+ }
+ FabState.SEND -> {
+ chatSend.apply {
+ setImageResource(R.drawable.ic_send)
+
+ setOnClickListener {
+ presenter.sendMessage(chatMessage.text.toString(), attachments)
+ }
+
+ setOnLongClickListener {
+ presenter.sendMessage(chatMessage.text.toString(), attachments, false)
+ true
+ }
+ }
+ }
+ FabState.EDIT -> {
+ chatSend.apply {
+ setImageResource(R.drawable.ic_done)
+
+ setOnClickListener {
+ //editMessage()
+ }
+
+ setOnLongClickListener {
+ performClick()
+ true
+ }
+ }
+
+ }
+ FabState.DELETE -> {
+ chatSend.apply {
+ setImageResource(R.drawable.ic_trash_outline)
+
+ chatSend.setOnClickListener {
+ //deleteMessage
+ }
+
+ chatSend.setOnLongClickListener {
+ performClick()
+ true
+ }
+ }
+ }
+ FabState.BLOCKED -> {
+ chatSend.apply {
+ isClickable = false
+ setImageResource(R.drawable.ic_lock)
+ }
+ }
+ }
+ }
+
+ override fun showChatPanel() {
+ chatPanel.isVisible = true
+ }
+
+ override fun hideChatPanel() {
+ chatPanel.isVisible = false
+ }
+
+ override fun setWritingAllowed(allowed: Boolean) {
+ if (allowed) {
+ fabState = FabState.VOICE
+
+ chatSend.imageTintList = ColorStateList.valueOf(color(R.color.accent))
+
+ chatMessage.isEnabled = true
+
+ chatPanel.setBackgroundResource(R.drawable.chat_panel_background)
+ } else {
+ fabState = FabState.BLOCKED
+
+ chatSend.imageTintList = ColorStateList.valueOf(Color.WHITE)
+
+ chatMessage.isEnabled = false
+ chatMessage.setHintTextColor(Color.WHITE)
+ chatMessage.setHint(R.string.no_access)
+
+ chatPanel.setBackgroundResource(R.drawable.chat_panel_background_blocked)
+ }
+ }
+
+ override fun setChatInfo(info: String) {
+ chatInfo.text = info
+ chatInfo.isVisible = info.isNotEmpty()
+ }
+
+ override fun openProfile(conversation: VKConversation) {
+ conversation.let {
+ val profileDialog = ProfileDialog(it, title)
+ profileDialog.show(supportFragmentManager, ProfileDialog.TAG)
+ }
+ }
+
+ override fun showErrorLoadConversationAlert() {
+ val builder = AlertDialog.Builder(this)
+ builder.setTitle(R.string.error_occurred)
+ builder.setMessage(R.string.error_loading_message)
+ builder.setPositiveButton(R.string.retry) { _, _ ->
+ presenter.loadConversation(peerId)
+ }
+ builder.setNegativeButton(R.string.no) { _, _ -> onBackPressed() }
+ builder.setCancelable(false)
+ builder.show()
+ }
+
+ override fun showVoiceRecordingTip() {
+ Toast.makeText(this, R.string.voice_record_tip, Toast.LENGTH_LONG).show()
+ }
+
+ override fun setMessageText(text: String) {
+ chatMessage.setText(text)
+ }
+
+ override fun showErrorSnackbar(t: Throwable) {
+ ViewUtils.showErrorSnackbar(getRootView(), t)
+ }
+
+ override fun prepareNoItemsView() {
+ }
+
+ override fun showNoItemsView() {
+ noItemsView.isVisible = true
+ }
+
+ override fun hideNoItemsView() {
+ noItemsView.isVisible = false
+ }
+
+ override fun prepareNoInternetView() {
+ }
+
+ override fun showNoInternetView() {
+ noInternetView.isVisible = true
+ }
+
+ override fun hideNoInternetView() {
+ noInternetView.isVisible = false
+ }
+
+ override fun prepareErrorView() {
+ }
+
+ override fun showErrorView() {
+ errorView.isVisible = true
+ }
+
+ override fun hideErrorView() {
+ errorView.isVisible = false
+ }
+
+ override fun showProgressBar() {
+ progressBar.isVisible = true
+ }
+
+ override fun hideProgressBar() {
+ progressBar.isVisible = false
+ }
+
+ override fun showRefreshLayout() {
+ refreshLayout.isRefreshing = true
+ }
+
+ override fun hideRefreshLayout() {
+ refreshLayout.isRefreshing = false
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt
new file mode 100644
index 00000000..e2711beb
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt
@@ -0,0 +1,44 @@
+package ru.melod1n.project.vkm.activity
+
+import android.os.Bundle
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.common.FragmentSwitcher
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable
+import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint
+import ru.melod1n.project.vkm.fragment.FragmentSettings
+import ru.melod1n.project.vkm.widget.Toolbar
+
+class SettingsActivity : BaseActivity() {
+
+ private lateinit var toolbar: Toolbar
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+ initViews()
+
+ setSupportActionBar(toolbar)
+
+ toolbar.navigationIcon = drawable(R.drawable.ic_arrow_back).tint(color(R.color.accent))
+
+ toolbar.setNavigationClickListener { onBackPressed() }
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.fragmentContainer, FragmentSettings()).commitNow()
+ }
+
+ private fun initViews() {
+ toolbar = findViewById(R.id.toolbar)
+ }
+
+ override fun onBackPressed() {
+ val currentFragment = FragmentSwitcher.getCurrentFragment(supportFragmentManager) ?: return
+
+ if (currentFragment.javaClass == FragmentSettings::class.java && (currentFragment as FragmentSettings).onBackPressed()) {
+ super.onBackPressed()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt
new file mode 100644
index 00000000..f6fbf3a9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt
@@ -0,0 +1,82 @@
+package ru.melod1n.project.vkm.activity
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.AppCompatEditText
+import com.google.android.material.button.MaterialButton
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.base.BaseActivity
+
+@SuppressLint("InflateParams")
+class StartActivity : BaseActivity() {
+
+ private lateinit var startEnter: MaterialButton
+ private lateinit var startLoginSettings: MaterialButton
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_start)
+ initViews()
+
+ prepareEnterButton()
+ }
+
+ private fun initViews() {
+ startEnter = findViewById(R.id.startEnter)
+ startLoginSettings = findViewById(R.id.startLoginSettings)
+ }
+
+ private fun prepareEnterButton() {
+ startEnter.setOnClickListener {
+ startActivity(Intent(this, LoginActivity::class.java))
+ }
+
+ startEnter.setOnLongClickListener {
+ showUserIdTokenDialog()
+ true
+ }
+
+ startLoginSettings.setOnClickListener {
+ Toast.makeText(this, R.string.in_progress_placeholder, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun showUserIdTokenDialog() {
+ AlertDialog.Builder(this).apply {
+ setTitle(R.string.custom_data)
+
+ val view = LayoutInflater.from(this@StartActivity)
+ .inflate(R.layout.activity_login_custom_data, null, false) as View
+ setView(view)
+
+ val userId = view.findViewById(R.id.customDataUserId)
+ val token = view.findViewById(R.id.customDataToken)
+
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ if (userId.text.toString().isEmpty() || token.text.toString().isEmpty())
+ return@setPositiveButton
+ val id = userId.text.toString().toInt()
+ val accessToken = token.text.toString()
+
+ if (id < 1) return@setPositiveButton
+
+ UserConfig.userId = id
+ UserConfig.token = accessToken
+ UserConfig.save()
+
+ finish()
+ startActivity(Intent(this@StartActivity, MainActivity::class.java))
+ }
+
+ setCancelable(false)
+ setNegativeButton(android.R.string.cancel, null)
+ }.show()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt
new file mode 100644
index 00000000..8d13fe05
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt
@@ -0,0 +1,350 @@
+package ru.melod1n.project.vkm.activity
+
+import android.app.Activity
+import android.app.DownloadManager
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.util.Log
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.FileProvider
+import androidx.core.text.HtmlCompat
+import androidx.core.view.isVisible
+import com.github.rahatarmanahmed.cpv.CircularProgressView
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.base.BaseActivity
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.common.UpdateManager
+import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable
+import ru.melod1n.project.vkm.extensions.FloatExtensions.int
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.model.NewUpdateInfo
+import ru.melod1n.project.vkm.receiver.DownloadUpdateReceiver
+import ru.melod1n.project.vkm.util.AndroidUtils
+import ru.melod1n.project.vkm.util.TimeUtils
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+class UpdateActivity : BaseActivity() {
+
+ companion object {
+ private const val FILE_BASE_PATH = "file://"
+ private const val MIME_TYPE = "application/vnd.android.package-archive"
+ private const val PROVIDER_PATH = ".provider"
+ }
+
+ private var isChecking = false
+ private var isNewUpdate = false
+ private var isDownloading = false
+
+ private var downloadId = 0L
+
+ private var lastCheckTime = 0L
+
+ private var newUpdate = NewUpdateInfo()
+
+ private lateinit var updateCheckUpdates: ExtendedFloatingActionButton
+ private lateinit var updateState: TextView
+ private lateinit var updateVersion: TextView
+ private lateinit var updateInfo: TextView
+ private lateinit var updateInfoLayout: LinearLayout
+ private lateinit var updateProgress: LinearLayout
+ private lateinit var updateProgressBar: CircularProgressView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_update)
+ initViews()
+
+ updateProgressBar.maxProgress = 100F
+
+ lastCheckTime = AppGlobal.preferences.getLong("updateCheckTime", 0)
+
+ refreshState()
+
+ checkUpdates()
+
+ updateCheckUpdates.setOnClickListener {
+ lastCheckTime = System.currentTimeMillis()
+ AppGlobal.preferences.edit().putLong("updateCheckTime", lastCheckTime).apply()
+
+ checkUpdates()
+ }
+ }
+
+ private fun initViews() {
+ updateCheckUpdates = findViewById(R.id.updateCheckUpdates)
+ updateInfo = findViewById(R.id.updateInfo)
+ updateVersion = findViewById(R.id.updateVersion)
+ updateState = findViewById(R.id.updateState)
+ updateInfoLayout = findViewById(R.id.updateInfoLayout)
+ updateProgress = findViewById(R.id.updateProgress)
+ updateProgressBar = updateProgress.getChildAt(0) as CircularProgressView
+ }
+
+ private fun installUpdate(context: Activity, file: File) {
+ val install = Intent(Intent.ACTION_VIEW)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ install.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
+ install.data = FileProvider.getUriForFile(
+ this,
+ BuildConfig.APPLICATION_ID + PROVIDER_PATH,
+ file
+ )
+ } else {
+ install.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ install.setDataAndType(Uri.fromFile(file), MIME_TYPE)
+ }
+
+ context.startActivity(install)
+// context.finishAffinity()
+ }
+
+ private fun downloadUpdate() {
+ checkIsInstallingAllowed()
+
+ val timer = Timer()
+
+ updateCheckUpdates.shrink()
+ updateCheckUpdates.isClickable = false
+
+ isDownloading = true
+ refreshState()
+
+ TaskManager.execute {
+ val apkName = newUpdate.version
+
+ val destination =
+ getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/$apkName.apk"
+
+ val uri = Uri.parse("$FILE_BASE_PATH$destination")
+
+ val file = File(destination)
+ if (file.exists()) file.delete()
+
+ val request = DownloadManager.Request(Uri.parse(newUpdate.downloadLink))
+
+ request.setTitle("${getString(R.string.app_name)} ${apkName}.apk")
+ request.setMimeType(MIME_TYPE)
+ request.setDestinationUri(uri)
+ request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
+
+ val receiver = DownloadUpdateReceiver()
+ receiver.listener = object : OnResponseListener {
+ override fun onResponse(response: Any?) {
+ timer.cancel()
+
+ installUpdate(this@UpdateActivity, file)
+
+ unregisterReceiver(receiver)
+
+ runOnUiThread {
+ updateProgressBar.isIndeterminate = true
+
+ updateCheckUpdates.extend()
+ updateCheckUpdates.isClickable = true
+
+ isDownloading = false
+ refreshState()
+ }
+ }
+
+ override fun onError(t: Throwable) {
+ }
+ }
+
+ registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
+
+ downloadId = AppGlobal.downloadManager.enqueue(request)
+
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ val query = DownloadManager.Query()
+ query.setFilterById(downloadId)
+
+ val cursor = AppGlobal.downloadManager.query(query)
+ if (cursor.moveToFirst()) {
+ val sizeIndex =
+ cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
+ val downloadedIndex =
+ cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
+ val size = cursor.getInt(sizeIndex)
+ val downloaded = cursor.getInt(downloadedIndex)
+
+ val progress = if (size != -1) (downloaded * 100.0F / size) else 0.0F
+
+ Log.d("Downloading update", "progress $progress%")
+
+ if (progress.int() > 0) {
+ runOnUiThread {
+ if (updateProgressBar.isIndeterminate) {
+ updateProgressBar.isIndeterminate = false
+ updateProgressBar.stopAnimation()
+ }
+
+ updateProgressBar.progress = progress
+ }
+ }
+ }
+ }
+
+ }, 0, 1000)
+ }
+ }
+
+ private fun checkUpdates() {
+ if (isChecking) return
+
+ isChecking = true
+ refreshState()
+
+ UpdateManager.checkUpdates(object : UpdateManager.OnUpdateListener {
+ override fun onNewUpdate(updateInfo: NewUpdateInfo) {
+ isChecking = false
+ isNewUpdate = true
+
+ this@UpdateActivity.newUpdate = updateInfo
+
+ refreshState()
+ }
+
+ override fun onNoUpdates() {
+ isNewUpdate = false
+ isChecking = false
+
+ this@UpdateActivity.newUpdate = NewUpdateInfo()
+
+ refreshState()
+ }
+ })
+ }
+
+ private fun checkIsInstallingAllowed() {
+ if (!AndroidUtils.isCanInstallUnknownApps(this)) {
+ val builder = AlertDialog.Builder(this)
+ builder.setTitle(R.string.warning)
+ builder.setMessage(R.string.update_unknown_sources_disabled_message)
+ builder.setPositiveButton(R.string.yes) { _, _ ->
+ AndroidUtils.openInstallUnknownAppsScreen(this)
+ }
+ builder.setNegativeButton(R.string.no, null)
+ builder.show()
+ }
+ }
+
+ private fun refreshState() {
+ when {
+ isChecking -> {
+ updateState.text = getString(R.string.update_state_checking)
+
+ setAlpha(updateInfoLayout, true)
+ setAlpha(updateProgress, false)
+ setAlpha(updateCheckUpdates, true)
+ }
+ isDownloading -> {
+ updateState.text = getString(R.string.update_state_downloading)
+
+ setAlpha(updateInfoLayout, true)
+ setAlpha(updateProgress, false)
+ setAlpha(updateCheckUpdates, true)
+ }
+ else -> {
+ if (isNewUpdate) {
+ updateCheckUpdates.text = getString(R.string.update_download)
+ updateCheckUpdates.icon = drawable(R.drawable.ic_file_download)
+ } else {
+ updateCheckUpdates.text = getString(R.string.update_check_updates)
+ updateCheckUpdates.icon = drawable(R.drawable.ic_refresh)
+ }
+
+ updateCheckUpdates.setOnClickListener {
+ if (isNewUpdate) {
+ downloadUpdate()
+ } else {
+ checkUpdates()
+ }
+ }
+
+ updateState.text =
+ getString(if (isNewUpdate) R.string.update_state_update_available else R.string.update_state_no_updates)
+
+ updateVersion.text =
+ if (isNewUpdate)
+ getString(
+ R.string.update_new_version,
+ newUpdate.version,
+ newUpdate.code
+ )
+ else getString(
+ R.string.update_current_version,
+ AppGlobal.versionName,
+ AppGlobal.versionCode
+ )
+
+ updateInfo.text =
+ when {
+ isNewUpdate -> if (newUpdate.changelog.isEmpty()) "" else getString(
+ R.string.update_changelog,
+ HtmlCompat.fromHtml(
+ newUpdate.changelog,
+ HtmlCompat.FROM_HTML_MODE_LEGACY
+ )
+ )
+ lastCheckTime.toString().isEmpty() || lastCheckTime == 0L -> ""
+ else -> getString(R.string.update_last_check_time, getCheckTime())
+ }
+
+ setAlpha(updateInfoLayout, false)
+ setAlpha(updateProgress, true)
+ setAlpha(updateCheckUpdates, false)
+ }
+ }
+ }
+
+ private fun getCheckTime(): String {
+ val time = lastCheckTime
+
+ val lastTime = TimeUtils.removeTime(Date(time))
+ val currentTime = TimeUtils.removeTime(Date(System.currentTimeMillis()))
+
+ val format = if (currentTime > lastTime) {
+ "dd.MM.yyyy HH:mm"
+ } else {
+ "HH:mm"
+ }
+
+ return SimpleDateFormat(format, Locale.getDefault()).format(time)
+ }
+
+ private fun setAlpha(view: View, toZero: Boolean) {
+ if (toZero) {
+ view.animate()
+ .alpha(0F)
+ .setDuration(250)
+ .withEndAction { view.isVisible = false }
+ .start()
+ } else {
+ view.animate()
+ .alpha(1F)
+ .setDuration(250)
+ .withStartAction { view.isVisible = true }
+ .start()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt
new file mode 100644
index 00000000..377c7250
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt
@@ -0,0 +1,255 @@
+package ru.melod1n.project.vkm.activity.ui.presenter
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.activity.ui.repository.MessagesRepository
+import ru.melod1n.project.vkm.activity.ui.view.MessagesView
+import ru.melod1n.project.vkm.adapter.MessagesAdapter
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.api.model.VKMessage
+import ru.melod1n.project.vkm.api.model.VKModel
+import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener
+import ru.melod1n.project.vkm.base.mvp.MvpPresenter
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.event.EventInfo
+import ru.melod1n.project.vkm.listener.ItemClickListener
+import ru.melod1n.project.vkm.listener.ItemLongClickListener
+import kotlin.random.Random
+
+class MessagesPresenter(viewState: MessagesView) :
+ MvpPresenter(
+ viewState,
+ MessagesRepository::class.java.name
+ ),
+ ItemClickListener,
+ ItemLongClickListener,
+ TaskManager.OnEventListener {
+
+ companion object {
+ const val DEFAULT_MESSAGES_COUNT = 30
+ }
+
+ private lateinit var adapter: MessagesAdapter
+ private lateinit var conversation: VKConversation
+
+ private var peerId: Int = -1
+
+ private var lastMessageText: String = ""
+
+ private lateinit var recyclerView: RecyclerView
+
+ override fun destroy() {
+ adapter.destroy()
+ }
+
+ fun setup(peerId: Int, recyclerView: RecyclerView) {
+ this.peerId = peerId
+ this.recyclerView = recyclerView
+ this.context = recyclerView.context
+
+ viewState.showProgressBar()
+ getCachedConversation(peerId)
+ }
+
+ fun updateData() {
+ adapter.clear()
+ loadMessages(peerId)
+ }
+
+ fun openProfile() {
+ viewState.openProfile(conversation)
+ }
+
+ private fun createAdapter() {
+ adapter = MessagesAdapter(context!!, arrayListOf(), conversation).also {
+ it.itemClickListener = this
+ it.itemLongClickListener = this
+ }
+
+ recyclerView.adapter = adapter
+ }
+
+ private fun getCachedConversation(peerId: Int) {
+ repository.getCachedConversation(peerId, object : MvpOnLoadListener {
+ override fun onResponse(response: VKConversation) {
+ conversation = response
+
+ createAdapter()
+ refreshConversation(response)
+
+ getCachedMessages(peerId, 0, DEFAULT_MESSAGES_COUNT,
+ object : MvpOnLoadListener {
+ override fun onResponse(response: Any?) {
+ loadConversation(peerId)
+ loadMessages(peerId)
+ }
+
+ override fun onError(t: Throwable) {
+ loadConversation(peerId)
+ loadMessages(peerId)
+ }
+ })
+ }
+
+ override fun onError(t: Throwable) {
+ loadConversation(peerId)
+ loadMessages(peerId)
+ }
+ })
+ }
+
+ fun loadConversation(peerId: Int) {
+ if (adapter.isNotEmpty()) {
+ viewState.hideProgressBar()
+ }
+
+ repository.loadConversation(peerId, object : MvpOnLoadListener {
+
+ override fun onResponse(response: VKConversation) {
+ conversation = response
+
+ createAdapter()
+ refreshConversation(response)
+ }
+
+ override fun onError(t: Throwable) {
+ viewState.hideProgressBar()
+ viewState.showErrorLoadConversationAlert()
+ }
+
+ })
+ }
+
+ private fun refreshConversation(conversation: VKConversation) {
+ checkIsWritingAllowed(conversation)
+
+ repository.getChatInfo(conversation, object : MvpOnLoadListener {
+ override fun onResponse(response: String) {
+ viewState.setChatInfo(response)
+ }
+
+ override fun onError(t: Throwable) {
+ viewState.setChatInfo(AppGlobal.resources.getString(R.string.error_obtain_chat_info))
+ }
+ })
+ }
+
+ private fun checkIsWritingAllowed(conversation: VKConversation) {
+ if (conversation.isGroupChannel) {
+ viewState.hideChatPanel()
+ return
+ }
+
+ viewState.showChatPanel()
+ viewState.setWritingAllowed(conversation.isAllowed)
+ }
+
+ private fun getCachedMessages(
+ peerId: Int,
+ offset: Int = 0,
+ count: Int = DEFAULT_MESSAGES_COUNT,
+ listener: MvpOnLoadListener? = null
+ ) {
+ repository.getCachedMessages(peerId, offset, count,
+ object : MvpOnLoadListener> {
+ override fun onResponse(response: ArrayList) {
+ viewState.hideProgressBar()
+ fillAdapter(response, offset)
+
+ listener?.onResponse(null)
+ }
+
+ override fun onError(t: Throwable) {
+ if (adapter.isEmpty()) {
+ viewState.showProgressBar()
+ }
+
+ listener?.onError(t)
+ }
+ })
+ }
+
+ private fun loadMessages(peerId: Int, offset: Int = 0, count: Int = DEFAULT_MESSAGES_COUNT) {
+ repository.loadMessages(peerId, offset, count,
+ object : MvpOnLoadListener> {
+ override fun onResponse(response: ArrayList) {
+ fillAdapter(response, offset)
+ }
+
+ override fun onError(t: Throwable) {
+
+ }
+
+ })
+ }
+
+ private fun fillAdapter(
+ messages: ArrayList,
+ offset: Int
+ ) {
+ if (adapter.isEmpty()) adapter.isNotCachedValues = true
+ if (offset == 0) {
+ adapter.updateValues(messages)
+ } else {
+ adapter.addAll(messages)
+ }
+
+ adapter.notifyDataSetChanged()
+
+ if (offset == 0) recyclerView.scrollToPosition(adapter.itemCount - 1)
+ }
+
+ override fun onItemClick(position: Int) {
+
+ }
+
+ override fun onItemLongClick(position: Int) {
+
+ }
+
+ override fun onNewEvent(info: EventInfo<*>) {
+
+ }
+
+ fun sendMessage(
+ text: String = "",
+ attachments: ArrayList = arrayListOf(),
+ scrollToBottom: Boolean = true
+ ) {
+ lastMessageText = text
+
+ val message = VKMessage().also {
+ it.date = (System.currentTimeMillis() / 1000).toInt()
+ it.text = text
+ it.isOut = true
+ it.peerId = peerId
+ it.fromId = UserConfig.userId
+ it.randomId = Random.nextInt()
+ }
+
+ viewState.setMessageText("")
+
+ adapter.addMessage(message, true, scrollToBottom)
+
+ repository.sendMessage(peerId, text, message.randomId, object : MvpOnLoadListener {
+ override fun onResponse(response: Int) {
+ message.messageId = response
+
+ TaskManager.execute { MemoryCache.put(message) }
+ TaskManager.loadMessage(VKApiKeys.UPDATE_MESSAGE, response)
+ }
+
+ override fun onError(t: Throwable) {
+ viewState.showErrorSnackbar(t)
+
+ viewState.setMessageText(lastMessageText)
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt
new file mode 100644
index 00000000..4e477cef
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt
@@ -0,0 +1,175 @@
+package ru.melod1n.project.vkm.activity.ui.repository
+
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.VKApi
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.api.model.VKGroup
+import ru.melod1n.project.vkm.api.model.VKMessage
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.api.util.VKUtil
+import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener
+import ru.melod1n.project.vkm.base.mvp.MvpRepository
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.util.ArrayUtils
+import java.util.*
+
+class MessagesRepository : MvpRepository() {
+
+ fun loadMessages(
+ peerId: Int,
+ offset: Int,
+ count: Int,
+ listener: MvpOnLoadListener>
+ ) {
+ TaskManager.execute {
+ VKApi.messages()
+ .getHistory()
+ .peerId(peerId)
+ .reversed(false)
+ .extended(true)
+ .fields(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS)
+ .offset(offset)
+ .count(count)
+ .executeArray(
+ VKMessage::class.java,
+ object : OnResponseListener> {
+ override fun onResponse(response: ArrayList) {
+ TaskManager.execute {
+ cacheLoadedMessages(response)
+
+ MemoryCache.putUsers(VKMessage.profiles)
+ MemoryCache.putGroups(VKMessage.groups)
+ MemoryCache.putConversations(VKMessage.conversations)
+
+ VKUtil.sortMessagesByDate(response, false)
+
+ sendResponse(listener, response)
+ }
+ }
+
+ override fun onError(t: Throwable) {
+ sendError(listener, t)
+ }
+ })
+ }
+ }
+
+ fun getCachedMessages(
+ peerId: Int, offset: Int, count: Int,
+ listener: MvpOnLoadListener>
+ ) {
+ TaskManager.execute {
+ val messages = MemoryCache.getMessagesByPeerId(peerId).asArrayList()
+
+ if (messages.isEmpty()) {
+ sendError(listener, NullPointerException("Messages is empty"))
+ return@execute
+ }
+
+ VKUtil.sortMessagesByDate(messages, false)
+
+ val preparedMessages = ArrayUtils.cut(messages, offset, count)
+
+ sendResponseArray(listener, preparedMessages)
+ }
+ }
+
+ fun getCachedConversation(peerId: Int, listener: MvpOnLoadListener) {
+ TaskManager.execute {
+ val conversation = MemoryCache.getConversationById(peerId)
+
+ if (conversation == null) {
+ sendError(
+ listener,
+ NullPointerException("Conversation is not cached at the moment")
+ )
+ } else {
+ sendResponse(listener, conversation)
+ }
+ }
+ }
+
+ fun loadConversation(peerId: Int, listener: MvpOnLoadListener) {
+ TaskManager.loadConversation(
+ VKApiKeys.UPDATE_CONVERSATION,
+ peerId,
+ object : OnResponseListener {
+ override fun onResponse(response: VKConversation) {
+ sendResponse(listener, response)
+ }
+
+ override fun onError(t: Throwable) {
+ sendError(listener, t)
+ }
+ })
+ }
+
+ fun getChatInfo(conversation: VKConversation, listener: MvpOnLoadListener) {
+ when (conversation.type) {
+ VKConversation.TYPE_CHAT -> {
+ sendResponse(
+ listener,
+ AppGlobal.resources.getString(
+ if (conversation.isGroupChannel)
+ R.string.group_channel_members
+ else R.string.chat_members,
+ conversation.membersCount
+ )
+ )
+ }
+ VKConversation.TYPE_USER -> {
+ val user = VKUtil.searchUser(conversation.conversationId,
+ object : OnResponseListener {
+ override fun onResponse(response: VKUser) {
+ sendResponse(listener, VKUtil.getUserOnline(response))
+ }
+
+ override fun onError(t: Throwable) {
+ sendError(listener, t)
+ }
+ })
+
+ user?.let {
+ sendResponse(listener, VKUtil.getUserOnline(it))
+ }
+ }
+ else -> {
+ sendResponse(listener, "")
+ }
+ }
+ }
+
+ fun sendMessage(
+ peerId: Int,
+ message: String,
+ randomId: Int,
+ listener: MvpOnLoadListener
+ ) {
+ TaskManager.execute {
+ VKApi.messages()
+ .send()
+ .peerId(peerId)
+ .message(message)
+ .randomId(randomId)
+ .executeArray(Int::class.java, object : OnResponseListener> {
+ override fun onResponse(response: ArrayList) {
+ val messageId = response[0]
+ sendResponse(listener, messageId)
+ }
+
+ override fun onError(t: Throwable) {
+ sendError(listener, t)
+ }
+ })
+ }
+ }
+
+ private fun cacheLoadedMessages(messages: ArrayList) {
+ MemoryCache.putMessages(messages)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt
new file mode 100644
index 00000000..2cb95c39
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt
@@ -0,0 +1,24 @@
+package ru.melod1n.project.vkm.activity.ui.view
+
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.base.mvp.MvpView
+
+interface MessagesView : MvpView {
+
+ fun showChatPanel()
+
+ fun hideChatPanel()
+
+ fun setWritingAllowed(allowed: Boolean)
+
+ fun setChatInfo(info: String)
+
+ fun openProfile(conversation: VKConversation)
+
+ fun showErrorLoadConversationAlert()
+
+ fun showVoiceRecordingTip()
+
+ fun setMessageText(text: String)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt
new file mode 100644
index 00000000..b4dc05fe
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt
@@ -0,0 +1,724 @@
+package ru.melod1n.project.vkm.adapter
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.text.SpannableString
+import android.text.TextUtils
+import android.text.style.ForegroundColorSpan
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.adapter.diffutil.ConversationsCallback
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.api.model.VKGroup
+import ru.melod1n.project.vkm.api.model.VKMessage
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.api.util.VKUtil
+import ru.melod1n.project.vkm.base.BaseAdapter
+import ru.melod1n.project.vkm.base.BaseHolder
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.event.EventInfo
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.widget.CircleImageView
+
+
+@Suppress("UNCHECKED_CAST")
+class ConversationsAdapter(
+ val recyclerView: RecyclerView,
+ values: ArrayList
+) : BaseAdapter(
+ recyclerView.context,
+ values
+), TaskManager.OnEventListener {
+
+ companion object {
+ private const val TAG = "ConversationsAdapter"
+ }
+
+ var isLoading: Boolean = false
+ private var currentPosition: Int = -1
+
+ init {
+ TaskManager.addOnEventListener(this)
+ }
+
+ override fun destroy() {
+ TaskManager.removeOnEventListener(this)
+ }
+
+ override fun onNewEvent(info: EventInfo<*>) {
+ when (info.key) {
+ VKApiKeys.NEW_MESSAGE -> addMessage(info.data as VKMessage)
+ VKApiKeys.EDIT_MESSAGE -> editMessage(info.data as VKMessage)
+ VKApiKeys.RESTORE_MESSAGE -> restoreMessage(info.data as VKMessage)
+ VKApiKeys.READ_MESSAGE -> readMessage(
+ (info.data as Array)[0],
+ (info.data as Array)[1]
+ )
+ VKApiKeys.DELETE_MESSAGE -> deleteMessage(
+ (info.data as Array)[0],
+ (info.data as Array)[1]
+ )
+
+ VKApiKeys.UPDATE_CONVERSATION -> updateConversation(info.data as Int)
+ VKApiKeys.UPDATE_MESSAGE -> updateMessage(info.data as Int)
+ VKApiKeys.UPDATE_USER -> updateUsers(info.data as ArrayList)
+ VKApiKeys.UPDATE_GROUP -> updateGroups(info.data as ArrayList)
+
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationHolder {
+ return ConversationHolder(view(R.layout.item_conversation, parent))
+ }
+
+ override fun onBindViewHolder(
+ holder: ConversationHolder,
+ position: Int,
+ payloads: MutableList
+ ) {
+ currentPosition = position
+ initListeners(holder.itemView, position)
+ holder.bind(position, payloads)
+ }
+
+ fun notifyChanges(oldList: List, newList: List = values) {
+ val callback = ConversationsCallback(oldList, newList)
+ val diff = DiffUtil.calculateDiff(callback)
+
+ diff.dispatchUpdatesTo(this)
+ }
+
+ fun isLastItem() = currentPosition >= itemCount - 1
+
+ inner class ConversationHolder(v: View) : BaseHolder(v) {
+
+ private var text: TextView = v.findViewById(R.id.conversationText)
+ private var title: TextView = v.findViewById(R.id.conversationTitle)
+ private var avatar: ImageView = v.findViewById(R.id.conversationAvatar)
+ private var online: ImageView = v.findViewById(R.id.conversationUserOnline)
+ private var out: CircleImageView = v.findViewById(R.id.conversationOut)
+ private var counter: TextView = v.findViewById(R.id.conversationCounter)
+ private var date: TextView = v.findViewById(R.id.conversationDate)
+ private var type: ImageView = v.findViewById(R.id.conversationType)
+ private var userAvatar: ImageView = v.findViewById(R.id.conversationUserAvatar)
+ private var root: LinearLayout = v.findViewById(R.id.conversationRoot)
+
+ private val colorHighlight = context.color(R.color.accent)
+
+ override fun bind(position: Int) {
+ bind(position, mutableListOf())
+ }
+
+ fun bind(position: Int, payloads: MutableList) {
+ Log.d(TAG, "bind position: $position")
+
+ val conversation = this@ConversationsAdapter[position]
+
+ val lastMessage = conversation.lastMessage
+
+ TaskManager.execute {
+ val peerUser: VKUser? =
+ if (conversation.isUser()) VKUtil.searchUser(conversation.conversationId) else null
+
+ val peerGroup: VKGroup? =
+ if (conversation.isGroup()) VKUtil.searchGroup(conversation.conversationId) else null
+
+ val fromUser: VKUser? =
+ if (lastMessage.isFromUser()) VKUtil.searchUser(lastMessage.fromId) else null
+
+ val fromGroup: VKGroup? =
+ if (lastMessage.isFromGroup()) VKUtil.searchGroup(lastMessage.fromId) else null
+
+ conversation.peerUser = peerUser
+ conversation.peerGroup = peerGroup
+
+ lastMessage.fromUser = fromUser
+ lastMessage.fromGroup = fromGroup
+
+ post {
+ val dialogTitle = setTitle(conversation, peerUser, peerGroup)
+
+ if (payloads.isNotEmpty()) {
+ for (payload in payloads) {
+ when (payload) {
+ ConversationsCallback.CONVERSATION -> {
+ setUserOnline(conversation, peerUser)
+ prepareUserAvatar(
+ conversation,
+ lastMessage,
+ fromUser,
+ fromGroup
+ )
+ prepareAvatar(dialogTitle, conversation, peerUser, peerGroup)
+ setDialogType(conversation)
+ setIsRead(lastMessage, conversation)
+ setCounterBackground(conversation)
+ }
+ ConversationsCallback.MESSAGE -> {
+ prepareUserAvatar(
+ conversation,
+ lastMessage,
+ fromUser,
+ fromGroup
+ )
+ prepareAttachments(lastMessage)
+ setIsRead(lastMessage, conversation)
+ setDate(lastMessage)
+ }
+ ConversationsCallback.GROUP -> {
+ prepareAvatar(dialogTitle, conversation, peerUser, peerGroup)
+ }
+ ConversationsCallback.USER -> {
+ setUserOnline(conversation, peerUser)
+ prepareAvatar(dialogTitle, conversation, peerUser, peerGroup)
+ }
+ ConversationsCallback.EDIT_MESSAGE -> {
+ prepareUserAvatar(
+ conversation,
+ lastMessage,
+ fromUser,
+ fromGroup
+ )
+ prepareAttachments(lastMessage)
+ setIsRead(lastMessage, conversation)
+ setDate(lastMessage)
+ }
+ ConversationsCallback.DATE -> {
+ setDate(lastMessage)
+ }
+ ConversationsCallback.ONLINE -> {
+ setUserOnline(conversation, peerUser)
+ }
+ ConversationsCallback.ATTACHMENTS -> {
+ prepareAttachments(lastMessage)
+ }
+ ConversationsCallback.AVATAR -> {
+ prepareAvatar(dialogTitle, conversation, peerUser, peerGroup)
+ }
+ ConversationsCallback.USER_AVATAR -> {
+ prepareUserAvatar(
+ conversation,
+ lastMessage,
+ fromUser,
+ fromGroup
+ )
+ }
+ ConversationsCallback.READ -> {
+ setIsRead(lastMessage, conversation)
+ }
+ ConversationsCallback.NOTIFICATIONS -> {
+ setCounterBackground(conversation)
+ }
+ }
+ }
+
+ return@post
+ }
+
+ setUserOnline(conversation, peerUser)
+
+ prepareUserAvatar(conversation, lastMessage, fromUser, fromGroup)
+
+ prepareAvatar(dialogTitle, conversation, peerUser, peerGroup)
+
+ setDialogType(conversation)
+
+ prepareAttachments(lastMessage)
+
+ setIsRead(lastMessage, conversation)
+
+ setDate(lastMessage)
+
+ setCounterBackground(conversation)
+
+ root.isVisible = true
+ }
+ }
+ }
+
+ private fun setTitle(
+ conversation: VKConversation,
+ peerUser: VKUser?,
+ peerGroup: VKGroup?
+ ): String {
+ val dialogTitle = VKUtil.getTitle(conversation, peerUser, peerGroup)
+ title.text = dialogTitle
+
+ return dialogTitle
+ }
+
+ private fun setUserOnline(conversation: VKConversation, peerUser: VKUser?) {
+ val onlineIcon = VKUtil.getUserOnlineIcon(context, conversation, peerUser)
+
+ online.setImageDrawable(onlineIcon)
+ online.isVisible = onlineIcon != null
+ }
+
+ private fun prepareUserAvatar(
+ conversation: VKConversation,
+ lastMessage: VKMessage,
+ fromUser: VKUser?,
+ fromGroup: VKGroup?
+ ) {
+ if ((conversation.isChat() || lastMessage.isOut) && !conversation.isGroupChannel) {
+ userAvatar.isVisible = true
+
+ val avatar = VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup)
+
+ if (avatar.isEmpty()) {
+ userAvatar.setImageDrawable(ColorDrawable(Color.TRANSPARENT))
+ } else {
+ userAvatar.setImageURI(Uri.parse(avatar))
+ }
+
+// ImageUtil.loadImage(
+// VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup),
+// object : ImageUtil.OnLoadListener {
+// override fun onLoad(bitmap: Bitmap) {
+// userAvatar.setImageBitmap(bitmap)
+// }
+//
+// override fun onError(e: Exception) {
+//
+// }
+// }
+// )
+
+// ImageUtil.loadImage(
+// VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup),
+// userAvatar,
+// placeholderNormal
+// )
+ } else {
+ userAvatar.isVisible = false
+ userAvatar.setImageDrawable(null)
+ }
+ }
+
+ private fun prepareAvatar(
+ dialogTitle: String,
+ conversation: VKConversation,
+ peerUser: VKUser?,
+ peerGroup: VKGroup?
+ ) {
+ val dialogAvatarPlaceholder = VKUtil.getAvatarPlaceholder(context, dialogTitle)
+
+ avatar.setImageDrawable(dialogAvatarPlaceholder)
+
+ val avatarLink = VKUtil.getAvatar(conversation, peerUser, peerGroup)
+
+ if (avatarLink.isNotEmpty()) {
+ avatar.setImageURI(Uri.parse(avatarLink))
+ }
+
+// ImageUtil.loadImage(
+// VKUtil.getAvatar(conversation, peerUser, peerGroup),
+// object : ImageUtil.OnLoadListener {
+// override fun onLoad(bitmap: Bitmap) {
+// avatar.setImageBitmap(bitmap)
+// }
+//
+// override fun onError(e: Exception) {
+//
+// }
+// }
+// )
+
+// ImageUtil.loadImage(
+// VKUtil.getAvatar(conversation, peerUser, peerGroup),
+// avatar,
+// dialogAvatarPlaceholder
+// )
+ }
+
+ private fun setDialogType(conversation: VKConversation) {
+ val dDialogType = VKUtil.getDialogType(context, conversation)
+
+ type.setImageDrawable(dDialogType)
+ type.isVisible = dDialogType != null
+ }
+
+ private fun prepareAttachments(lastMessage: VKMessage) {
+ text.apply {
+ compoundDrawablePadding = 0
+ setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
+ }
+
+ if (lastMessage.action == null) {
+ when {
+ lastMessage.attachments.isNotEmpty() -> {
+ val attachmentString =
+ VKUtil.getAttachmentText(context, lastMessage.attachments)
+
+ val attachmentText =
+ if (lastMessage.text.isEmpty()) attachmentString else lastMessage.text
+
+ val startIndex =
+ if (lastMessage.text.isEmpty()) 0 else lastMessage.text.length
+
+ val span = SpannableString(attachmentText).apply {
+ setSpan(
+ ForegroundColorSpan(colorHighlight),
+ startIndex,
+ attachmentText.length,
+ 0
+ )
+ }
+
+ val attachmentDrawable =
+ VKUtil.getAttachmentDrawable(context, lastMessage.attachments)
+
+ text.apply {
+ text = span
+ setCompoundDrawablesRelativeWithIntrinsicBounds(
+ attachmentDrawable,
+ null,
+ null,
+ null
+ )
+ compoundDrawablePadding = 8
+ }
+ }
+ lastMessage.fwdMessages.isNotEmpty() -> {
+ val fwdText = VKUtil.getFwdText(context, lastMessage.getForwardedMessages())
+ val span = SpannableString(fwdText).apply {
+ setSpan(ForegroundColorSpan(colorHighlight), 0, fwdText.length, 0)
+ }
+
+ text.text = span
+ }
+ else -> {
+ text.text = if (text.maxLines == 1) lastMessage.text.replace(
+ "\n",
+ " "
+ ) else lastMessage.text
+ }
+ }
+ } else {
+ VKUtil.getActionText(context, lastMessage,
+ object : OnResponseListener {
+ override fun onResponse(response: String) {
+ val span = SpannableString(response).apply {
+ setSpan(
+ ForegroundColorSpan(colorHighlight),
+ 0,
+ response.length,
+ 0
+ )
+ }
+
+ text.text = span
+ }
+
+ override fun onError(t: Throwable) {
+ TODO("Not yet implemented")
+ }
+
+ })
+
+ }
+
+ if (lastMessage.attachments.isEmpty() && lastMessage.fwdMessages.isEmpty() && lastMessage.action == null && TextUtils.isEmpty(
+ lastMessage.text
+ )
+ ) {
+ val unknown = "..."
+ val span = SpannableString(unknown).apply {
+ setSpan(ForegroundColorSpan(colorHighlight), 0, unknown.length, 0)
+ }
+
+ text.text = span
+ }
+ }
+
+ private fun setIsRead(lastMessage: VKMessage, conversation: VKConversation) {
+ val isRead =
+ ((lastMessage.isOut && conversation.outRead == conversation.lastMessageId ||
+ !lastMessage.isOut && conversation.inRead == conversation.lastMessageId) && conversation.lastMessageId == lastMessage.messageId) && conversation.unreadCount == 0
+
+ if (isRead) {
+ counter.visibility = View.GONE
+ out.visibility = View.GONE
+ } else {
+ if (lastMessage.isOut) {
+ out.visibility = View.VISIBLE
+ counter.visibility = View.GONE
+ counter.text = ""
+ } else {
+ out.visibility = View.GONE
+ counter.visibility = View.VISIBLE
+ counter.text = conversation.unreadCount.toString()
+ }
+ }
+ }
+
+ private fun setDate(lastMessage: VKMessage) {
+ val dateText = VKUtil.getTime(context, lastMessage)
+ date.text = dateText
+ }
+
+ private fun setCounterBackground(conversation: VKConversation) {
+ counter.background.setTint(if (conversation.isNotificationsDisabled()) Color.GRAY else colorHighlight)
+ }
+ }
+
+ @Deprecated("Message is bad")
+ private fun addMessage(message: VKMessage) {
+ val index = searchConversationIndex(message.peerId)
+
+ val oldList = ArrayList(values)
+
+ if (index >= 0) {
+ val currentConversation = this[index]
+
+ val conversation = prepareConversation(currentConversation, message)
+
+ removeAt(index)
+ add(0, conversation)
+ notifyChanges(oldList)
+ } else {
+ TaskManager.loadConversation(
+ VKApiKeys.UPDATE_CONVERSATION,
+ message.peerId,
+ null
+ )
+
+ TaskManager.execute {
+ val cachedConversation = MemoryCache.getConversationById(message.peerId)
+ if (cachedConversation != null) {
+ add(0, prepareConversation(cachedConversation, message))
+ post { notifyChanges(oldList) }
+ return@execute
+ }
+
+ val tempConversations = VKConversation().apply {
+ conversationId = message.peerId
+
+ localId =
+ if (VKUtil.isChatId(conversationId)) conversationId - 2000000000 else conversationId
+ type =
+ if (conversationId < 0) VKConversation.TYPE_GROUP else if (conversationId > 2000000000) VKConversation.TYPE_CHAT else VKConversation.TYPE_USER
+
+ lastMessage = message
+ lastMessageId = message.messageId
+ }
+
+ add(0, tempConversations)
+
+ post { notifyChanges(oldList) }
+ }
+ }
+
+ val firstVisiblePosition =
+ (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
+
+ if (firstVisiblePosition <= 1) recyclerView.scrollToPosition(0)
+ }
+
+ private fun editMessage(message: VKMessage) {
+ val index = searchConversationIndex(message.peerId)
+ if (index == -1) return
+
+ val conversation = getItem(index)
+
+ if (conversation.lastMessageId != message.messageId) return
+
+ conversation.lastMessage = message
+
+ notifyItemChanged(index, ConversationsCallback.EDIT_MESSAGE)
+ }
+
+ private fun readMessage(peerId: Int, messageId: Int) {
+ val index = searchConversationIndex(peerId)
+ if (index == -1) return
+
+ val conversation = getItem(index)
+ val message = conversation.lastMessage
+
+ if (message.isInbox()) {
+ conversation.inRead = messageId
+ } else {
+ conversation.outRead = messageId
+ }
+
+ conversation.unreadCount = if (conversation.lastMessageId == messageId) {
+ 0
+ } else {
+ conversation.lastMessageId - messageId
+ }
+
+ notifyItemChanged(index, ConversationsCallback.READ)
+ }
+
+ @Deprecated("Need to rewrite")
+ private fun deleteMessage(peerId: Int, messageId: Int) {
+ return
+ val index = searchConversationIndex(peerId)
+ if (index == -1) return
+
+ val oldList = ArrayList(values)
+
+ val oldDialog = values[index]
+
+ val dialog = oldDialog.clone()
+
+ TaskManager.execute {
+ val cachedMessages = MemoryCache.getMessagesByPeerId(dialog.conversationId)
+ val messages = VKUtil.sortMessagesByDate(ArrayList(cachedMessages), true)
+
+ if (messages.isEmpty()) {
+ MemoryCache.deleteConversation(dialog.conversationId)
+
+ AppGlobal.post {
+ removeAt(index)
+ notifyChanges(oldList)
+ }
+ } else {
+ val lastMessage = messages[0]
+
+ dialog.lastMessageId = lastMessage.messageId
+ dialog.lastMessage = lastMessage
+
+ set(index, dialog)
+
+ VKUtil.sortConversationsByDate(values, true)
+
+ AppGlobal.post {
+ notifyChanges(oldList)
+ }
+ }
+ }
+ }
+
+ @Deprecated("Message is bad")
+ private fun restoreMessage(message: VKMessage) {
+ val index = searchConversationIndex(message.peerId)
+ if (index == -1) return
+
+ val oldList = ArrayList().apply { addAll(values) }
+ val oldDialog = values[index]
+
+ val dialog = oldDialog.clone()
+
+ TaskManager.execute {
+ val messages =
+ MemoryCache.getMessagesByPeerId(dialog.conversationId).apply { addMessage(message) }
+
+ VKUtil.sortMessagesByDate(ArrayList(messages), true)
+
+ val lastMessage = messages[0]
+
+ dialog.lastMessageId = lastMessage.messageId
+ dialog.lastMessage = lastMessage
+
+ set(index, dialog)
+
+ VKUtil.sortConversationsByDate(values, true)
+
+ AppGlobal.handler.post {
+ notifyChanges(oldList)
+
+// fragmentConversations.presenter.checkListIsEmpty(values)
+ }
+ }
+ }
+
+ private fun prepareConversation(
+ conversation: VKConversation,
+ newMessage: VKMessage
+ ): VKConversation {
+ conversation.lastMessage = newMessage
+ conversation.lastMessageId = newMessage.messageId
+
+ if (newMessage.isOut) {
+ conversation.unreadCount = 0
+ newMessage.isRead = false
+ } else {
+ conversation.unreadCount++
+ }
+
+ if (newMessage.peerId == newMessage.fromId && newMessage.fromId == UserConfig.userId) { //для лс
+ conversation.outRead = newMessage.messageId
+ }
+
+ return conversation
+ }
+
+ private fun searchConversationIndex(peerId: Int): Int {
+ for (i in values.indices) {
+ if (getItem(i).conversationId == peerId) return i
+ }
+ return -1
+ }
+
+ private fun searchMessageIndex(messageId: Int): Int {
+ for (i in values.indices) {
+ if (getItem(i).lastMessageId == messageId) return i
+ }
+ return -1
+ }
+
+ private fun updateConversation(peerId: Int) {
+ val index = searchConversationIndex(peerId)
+ if (index == -1) return
+
+ TaskManager.execute {
+ val conversation = MemoryCache.getConversationById(peerId) ?: return@execute
+
+ set(index, conversation)
+
+ AppGlobal.post { notifyItemChanged(index, ConversationsCallback.CONVERSATION) }
+ }
+ }
+
+ private fun updateGroups(groupIds: ArrayList) {
+ for (groupId in groupIds) {
+ val index = searchConversationIndex(groupId)
+ if (index == -1) return
+
+ notifyItemChanged(index)
+ }
+ }
+
+ private fun updateUsers(userIds: ArrayList) {
+ for (userId in userIds) {
+ val index = searchConversationIndex(userId)
+ if (index == -1) return
+
+ notifyItemChanged(index)
+ }
+ }
+
+ private fun updateMessage(messageId: Int) {
+ val index = searchMessageIndex(messageId)
+ if (index == -1) return
+
+
+ TaskManager.execute {
+ val conversation = getItem(index).clone()
+
+ conversation.apply {
+ lastMessageId = messageId
+ lastMessage = MemoryCache.getMessageById(messageId) ?: return@execute
+ }
+
+ AppGlobal.handler.post { notifyItemChanged(index, ConversationsCallback.MESSAGE) }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt
new file mode 100644
index 00000000..f7d3dd1b
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt
@@ -0,0 +1,581 @@
+package ru.melod1n.project.vkm.adapter
+
+import android.content.Context
+import android.graphics.Typeface
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.LinearLayoutManager
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.activity.MessagesActivity
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.*
+import ru.melod1n.project.vkm.api.util.VKUtil
+import ru.melod1n.project.vkm.base.BaseAdapter
+import ru.melod1n.project.vkm.base.BaseHolder
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.event.EventInfo
+import ru.melod1n.project.vkm.extensions.FloatExtensions.int
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.util.AndroidUtils
+import ru.melod1n.project.vkm.util.ImageUtils
+import ru.melod1n.project.vkm.widget.BoundedLinearLayout
+import ru.melod1n.project.vkm.widget.CircleImageView
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.math.abs
+
+
+@Suppress("UNCHECKED_CAST")
+class MessagesAdapter(
+ context: Context,
+ values: ArrayList,
+ var conversation: VKConversation
+
+) : BaseAdapter(context, values),
+ TaskManager.OnEventListener {
+
+ companion object {
+ private const val TYPE_FOOTER = 10101
+
+ private const val TYPE_NORMAL_IN = 7910
+ private const val TYPE_NORMAL_OUT = 7911
+
+ private const val TYPE_ATTACHMENT_IN = 7920
+ private const val TYPE_ATTACHMENT_OUT = 7921
+
+ private const val TYPE_ACTION = 7930
+
+ private const val TYPE_NORMAL_CHANNEL = 7940
+
+ const val TAG = "MessagesAdapter"
+ }
+
+ private var recyclerView = (context as MessagesActivity).recyclerView
+ private var layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ var isNotCachedValues = false
+
+ init {
+ TaskManager.addOnEventListener(this)
+ }
+
+ override fun destroy() {
+ TaskManager.removeOnEventListener(this)
+ }
+
+ override fun onNewEvent(info: EventInfo<*>) {
+ when (info.key) {
+ VKApiKeys.NEW_MESSAGE -> addMessage(info.data as VKMessage)
+
+ VKApiKeys.READ_MESSAGE -> readMessage(
+ (info.data as Array)[0],
+ (info.data as Array)[1]
+ )
+
+ VKApiKeys.RESTORE_MESSAGE -> restoreMessage(info.data as VKMessage)
+ VKApiKeys.EDIT_MESSAGE -> editMessage(info.data as VKMessage)
+ VKApiKeys.DELETE_MESSAGE -> deleteMessage(
+ (info.data as Array)[0],
+ (info.data as Array)[1]
+ )
+
+ VKApiKeys.UPDATE_MESSAGE -> updateMessage(info.data as Int)
+ VKApiKeys.UPDATE_USER -> updateUser(info.data as ArrayList)
+ VKApiKeys.UPDATE_GROUP -> updateGroup(info.data as ArrayList)
+
+ else -> return
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return values.size + 1
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ if (position == values.size) return TYPE_FOOTER
+
+ val message = getItem(position)
+
+ return when {
+ message.action != null -> TYPE_ACTION
+ conversation.isGroupChannel -> TYPE_NORMAL_CHANNEL
+ message.isOut && message.attachments.isEmpty() && message.fwdMessages.isEmpty() -> TYPE_NORMAL_OUT
+ !message.isOut && message.attachments.isEmpty() && message.fwdMessages.isEmpty() -> TYPE_NORMAL_IN
+ message.isOut && (message.attachments.isNotEmpty() || message.fwdMessages.isNotEmpty()) -> TYPE_ATTACHMENT_OUT
+ !message.isOut && (message.attachments.isNotEmpty() || message.fwdMessages.isNotEmpty()) -> TYPE_ATTACHMENT_IN
+ else -> 0
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, type: Int): Holder {
+ return when (type) {
+ TYPE_FOOTER -> FooterHolder(generateEmptyView())
+
+ TYPE_NORMAL_IN -> ItemNormalIn(view(R.layout.item_message_normal_in, viewGroup))
+ TYPE_NORMAL_OUT -> ItemNormalOut(view(R.layout.item_message_normal_out, viewGroup))
+
+ TYPE_ATTACHMENT_IN -> ItemAttachmentIn(
+ view(
+ R.layout.item_message_attachment_in,
+ viewGroup
+ )
+ )
+ TYPE_ATTACHMENT_OUT -> ItemAttachmentOut(
+ view(
+ R.layout.item_message_attachment_out,
+ viewGroup
+ )
+ )
+
+ TYPE_ACTION -> ItemAction(view(R.layout.item_message_action, viewGroup))
+
+ TYPE_NORMAL_CHANNEL -> ItemChannel(view(R.layout.item_message_channel, viewGroup))
+
+ else -> PlaceHolder(view(R.layout.item_message, viewGroup))
+ }
+ }
+
+ override fun onBindViewHolder(holder: Holder, position: Int) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "bind position: $position")
+ }
+
+ if (holder is FooterHolder) return
+
+ super.onBindViewHolder(holder, position)
+ if (!isNotCachedValues) return
+
+ val message = this[position]
+
+ if (message.isUnreaded()) {
+ TaskManager.readMessage(
+ VKApiKeys.READ_MESSAGE,
+ conversation.conversationId,
+ message.messageId
+ )
+ }
+ }
+
+ private fun generateEmptyView(): View {
+ return View(context).also {
+ it.isFocusable = false
+ it.isClickable = false
+ it.isEnabled = false
+ it.layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ if (conversation.isGroupChannel) 0 else AndroidUtils.px(74f).int()
+ )
+ }
+ }
+
+ inner class FooterHolder(v: View) : Holder(v) {
+ override fun bind(position: Int) {}
+ }
+
+ inner class ItemChannel(v: View) : ItemNormalIn(v) {
+ private val title: TextView = v.findViewById(R.id.channelTitle)
+
+ override fun bind(position: Int) {
+ val message = getItem(position)
+
+ ViewController().prepareDate(message, date)
+
+ val avatarString = conversation.photo100
+
+ val placeHolder = VKUtil.getAvatarPlaceholder(context, conversation.title)
+
+ avatar.setImageDrawable(placeHolder)
+ ImageUtils.loadImage(avatarString, avatar, placeHolder)
+
+ title.text = conversation.title
+
+ text.text = message.text
+
+ root.visibility = View.VISIBLE
+ }
+ }
+
+ inner class ItemAction(v: View) : Holder(v) {
+ private val text: TextView = v.findViewById(R.id.messageAction)
+
+ override fun bind(position: Int) {
+ val message = getItem(position)
+
+ TaskManager.execute {
+ val user = searchUser(message)
+ val group = searchGroup(message)
+
+ val name =
+ (if (group == null && !VKGroup.isGroupId(message.fromId)) user?.firstName else group?.name)
+ ?: "null"
+
+ VKUtil.getActionText(context, message, object : OnResponseListener {
+
+ override fun onResponse(response: String) {
+ val actionText = "$name $response"
+
+ val spannable = SpannableString(actionText)
+ spannable.setSpan(StyleSpan(Typeface.BOLD), 0, name.length, 0)
+
+ text.text = spannable
+ }
+
+ override fun onError(t: Throwable) {
+ }
+ })
+
+ post { text.isVisible = true }
+ }
+ }
+ }
+
+ open inner class ItemNormalIn(v: View) : NormalViewHolder(v) {
+
+ override fun bind(position: Int) {
+ val message = getItem(position)
+
+ TaskManager.execute {
+ val user = searchUser(message)
+ val group = searchGroup(message)
+
+ post {
+ ViewController().apply {
+ prepareText(message, bubble, text)
+ prepareDate(message, date)
+ prepareAvatar(message, avatar)
+ loadAvatarImage(message, user, group, avatar)
+ }
+
+ root.isVisible = true
+ }
+ }
+ }
+ }
+
+ inner class ItemAttachmentIn(v: View) : ItemNormalIn(v) {
+ val attachments: LinearLayout = v.findViewById(R.id.messageAttachments)
+
+ override fun bind(position: Int) {
+ super.bind(position)
+
+ val message = getItem(position)
+
+ AttachmentInflater.showAttachments(message, this)
+ }
+ }
+
+ open inner class ItemNormalOut(v: View) : NormalViewHolder(v) {
+
+ override fun bind(position: Int) {
+ val message = getItem(position)
+
+ TaskManager.execute {
+ val user = searchUser(message)
+ val group = searchGroup(message)
+
+ post {
+ ViewController().apply {
+ prepareText(message, bubble, text)
+ prepareDate(message, date)
+ prepareAvatar(message, avatar)
+ loadAvatarImage(message, user, group, avatar)
+ }
+
+ root.isVisible = true
+ }
+ }
+ }
+ }
+
+ inner class ItemAttachmentOut(v: View) : ItemNormalOut(v) {
+
+ val attachments: LinearLayout = v.findViewById(R.id.messageAttachments)
+
+ override fun bind(position: Int) {
+ super.bind(position)
+
+ val message = getItem(position)
+
+ AttachmentInflater.showAttachments(message, this)
+ }
+
+ }
+
+ abstract inner class NormalViewHolder(v: View) : Holder(v) {
+ protected val date: TextView = v.findViewById(R.id.messageDate)
+ protected val text: TextView = v.findViewById(R.id.messageText)
+ protected val root: LinearLayout = v.findViewById(R.id.messageRoot)
+ protected val bubble: BoundedLinearLayout = v.findViewById(R.id.messageBubble)
+ protected val avatar: CircleImageView = v.findViewById(R.id.messageAvatar)
+ }
+
+ object AttachmentInflater {
+ fun showAttachments(message: VKMessage, holder: NormalViewHolder) {
+ val attachments =
+ (if (holder is ItemAttachmentOut) holder.attachments else if (holder is ItemAttachmentIn) holder.attachments else null)
+ ?: return
+
+ if (message.fwdMessages.isNotEmpty() || message.attachments.isNotEmpty()) {
+ attachments.visibility = View.VISIBLE
+ attachments.removeAllViews()
+ } else {
+ attachments.visibility = View.GONE
+ }
+
+ if (message.attachments.isNotEmpty()) {
+ prepareAttachments(message, attachments)
+ }
+
+ if (message.fwdMessages.isNotEmpty()) {
+ prepareForwardedMessages(message, attachments)
+ }
+ }
+
+ private fun prepareAttachments(message: VKMessage, attachments: LinearLayout) {
+ for (attachment in message.attachments) {
+ when (attachment) {
+ is VKPhoto -> photo(message, attachments)
+ is VKVideo -> video(message, attachments)
+ is VKLink -> link(message, attachments)
+ is VKAudio -> audio(message, attachments)
+ is VKDoc -> doc(message, attachments)
+ }
+ }
+ }
+
+ private fun prepareForwardedMessages(message: VKMessage, attachments: LinearLayout) {
+
+ }
+
+ fun link(message: VKMessage, attachments: LinearLayout) {
+
+ }
+
+ fun video(message: VKMessage, attachments: LinearLayout) {
+
+ }
+
+ fun photo(message: VKMessage, attachments: LinearLayout) {
+ }
+
+ fun audio(message: VKMessage, attachments: LinearLayout) {
+
+ }
+
+ fun doc(message: VKMessage, attachments: LinearLayout) {
+
+ }
+ }
+
+ inner class ViewController {
+
+ fun prepareText(message: VKMessage, bubble: BoundedLinearLayout, text: TextView) {
+ val screenWidth = context.resources.displayMetrics.widthPixels
+ val boundedWidth = screenWidth - screenWidth / 5
+ bubble.maxWidth = boundedWidth
+
+ text.text = VKUtil.matchMentions(message.text)
+ }
+
+ fun prepareDate(message: VKMessage, date: TextView) {
+ var dateText =
+ SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
+
+ if (message.editTime > 0) {
+ dateText += ", ${
+ context.getString(R.string.edited)
+ .toLowerCase(Locale.getDefault())
+ }"
+ }
+
+ date.text = dateText
+ }
+
+ fun prepareAvatar(message: VKMessage, avatar: ImageView) {
+ avatar.isVisible = !message.isOut
+ }
+
+ fun loadAvatarImage(message: VKMessage, user: VKUser?, group: VKGroup?, avatar: ImageView) {
+ val dialogTitle = VKUtil.getMessageTitle(message, user, group)
+ val avatarPlaceholder = VKUtil.getAvatarPlaceholder(context, dialogTitle)
+
+ avatar.setImageDrawable(avatarPlaceholder)
+
+ val avatarString = VKUtil.getUserAvatar(message, user, group)
+
+ ImageUtils.loadImage(avatarString, avatar, avatarPlaceholder)
+ }
+
+ }
+
+ open inner class Holder(v: View) : BaseHolder(v) {
+ override fun bind(position: Int) {
+ }
+ }
+
+ inner class PlaceHolder(v: View) : NormalViewHolder(v)
+
+ private fun searchUser(message: VKMessage): VKUser? {
+ if (!message.isFromUser()) return null
+
+ return VKUtil.searchUser(message.fromId)
+ }
+
+ private fun searchGroup(message: VKMessage): VKGroup? {
+ if (!message.isFromGroup()) return null
+
+ return VKUtil.searchGroup(message.fromId)
+ }
+
+ private fun updateGroup(groupIds: ArrayList) {
+ for (groupId in groupIds) {
+ var index = -1
+
+ for (i in values.indices) {
+ val item = getItem(i)
+
+ if (abs(item.fromId) == groupId) {
+ index = i
+ break
+ }
+ }
+
+ if (index == -1) return
+
+ notifyItemChanged(index)
+ }
+ }
+
+ private fun updateUser(userIds: ArrayList) {
+ for (userId in userIds) {
+ var index = -1
+
+ for (i in values.indices) {
+ val item = getItem(i)
+
+ if (item.fromId == userId) {
+ index = i
+ break
+ }
+ }
+
+ if (index == -1) return
+ notifyItemChanged(index)
+ }
+ }
+
+ private fun updateMessage(messageId: Int) {
+ var index = -1
+
+ for (i in values.indices) {
+ val item = getItem(i)
+
+ if (item.messageId == messageId) {
+ index = i
+ break
+ }
+ }
+
+ if (index == -1) return
+
+ TaskManager.execute {
+ AppGlobal.database.messages.getById(messageId)?.let {
+ values[index] = it
+
+ post { notifyItemChanged(index) }
+ }
+ }
+ }
+
+ private fun searchMessagePosition(messageId: Int): Int {
+ for (i in values.indices) {
+ if (getItem(i).messageId == messageId) return i
+ }
+
+ return -1
+ }
+
+ private fun containsRandomId(randomId: Int): Boolean {
+ for (message in values) {
+ if (message.randomId == randomId) return true
+ }
+
+ return false
+ }
+
+ fun addMessage(message: VKMessage, fromApp: Boolean = false, withScroll: Boolean = false) {
+ val randomId = message.randomId
+ if (randomId > 0 && containsRandomId(message.randomId) || message.peerId != conversation.conversationId) return
+
+ add(message)
+
+ notifyDataSetChanged()
+
+ val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
+
+ if ((message.isInbox() && lastVisiblePosition >= itemCount - 2) || !fromApp || withScroll) {
+ recyclerView.scrollToPosition(itemCount - 1)
+ }
+ }
+
+ private fun readMessage(peerId: Int, messageId: Int) {
+ if (peerId != conversation.conversationId) return
+
+ val index = searchMessagePosition(messageId)
+ if (index == -1) return
+
+ val message = this[index]
+ message.isRead = true
+
+ notifyDataSetChanged()
+
+ if (message.isInbox()) {
+ conversation.inRead = messageId
+ } else {
+ conversation.outRead = messageId
+ }
+
+ conversation.unreadCount--
+
+ TaskManager.execute {
+ MemoryCache.put(message)
+ MemoryCache.put(conversation)
+ }
+ }
+
+ fun editMessage(message: VKMessage) {
+ val index = searchMessagePosition(message.messageId)
+ if (index == -1) return
+
+ set(index, message)
+ notifyDataSetChanged()
+ }
+
+ fun deleteMessage(messageId: Int, peerId: Int) {
+ if (peerId != conversation.conversationId) return
+
+ val index = searchMessagePosition(messageId)
+ if (index == -1) return
+
+ removeAt(index)
+ notifyDataSetChanged()
+ }
+
+ //TODO: кривое сообщение
+ fun restoreMessage(message: VKMessage) {
+ if (message.peerId != conversation.conversationId) return
+
+ updateValues(VKUtil.sortMessagesByDate(values.apply { add(message) }, false))
+ notifyDataSetChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt
new file mode 100644
index 00000000..2ebdc81d
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt
@@ -0,0 +1,35 @@
+package ru.melod1n.project.vkm.adapter
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.base.BaseAdapter
+import ru.melod1n.project.vkm.base.BaseHolder
+import ru.melod1n.project.vkm.item.SimpleMenuItem
+import java.util.*
+
+class SimpleItemAdapter(context: Context, values: ArrayList) :
+ BaseAdapter(context, values) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(view(R.layout.item_simple_menu, parent))
+ }
+
+ inner class ViewHolder(v: View) : BaseHolder(v) {
+
+ private val title: TextView = v.findViewById(R.id.profileItemTitle)
+ private val icon: ImageView = v.findViewById(R.id.profileItemIcon)
+
+ override fun bind(position: Int) {
+ val item = getItem(position)
+
+ title.text = item.title
+
+ icon.setImageDrawable(item.icon)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt
new file mode 100644
index 00000000..559cebce
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt
@@ -0,0 +1,68 @@
+package ru.melod1n.project.vkm.adapter
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.adapter.diffutil.UsersCallback
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.api.util.VKUtil
+import ru.melod1n.project.vkm.base.BaseAdapter
+import ru.melod1n.project.vkm.base.BaseHolder
+import ru.melod1n.project.vkm.util.ImageUtils
+import ru.melod1n.project.vkm.widget.CircleImageView
+
+class UsersAdapter(context: Context, values: ArrayList) :
+ BaseAdapter(context, values) {
+
+ var isLoading: Boolean = false
+ var currentPosition: Int = 0
+
+ fun isLastItem() = currentPosition >= itemCount - 1
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(view(R.layout.item_user, parent))
+ }
+
+ open inner class ViewHolder(v: View) : BaseHolder(v) {
+ private val avatar: CircleImageView = v.findViewById(R.id.userAvatar)
+ private val name: TextView = v.findViewById(R.id.userName)
+ private val online: ImageView = v.findViewById(R.id.userOnline)
+ private val onlineText: TextView = v.findViewById(R.id.userOnlineText)
+
+ override fun bind(position: Int) {
+ currentPosition = position
+
+ val user = getItem(position)
+
+ name.text = user.toString()
+
+ val avatarPlaceholder = VKUtil.getAvatarPlaceholder(context, user.toString())
+ avatar.setImageDrawable(avatarPlaceholder)
+
+ ImageUtils.loadImage(user.photo200, avatar, avatarPlaceholder)
+
+ online.isVisible = false
+
+ VKUtil.getUserOnlineIcon(context, user)?.let {
+ online.setImageDrawable(it)
+ online.isVisible = true
+ }
+
+ onlineText.text = VKUtil.getUserOnline(user)
+
+ //TODO: отладить открытие чата
+ }
+ }
+
+ fun notifyChanges(oldList: List, newList: List = values) {
+ val callback = UsersCallback(oldList, newList)
+ val diff = DiffUtil.calculateDiff(callback, false)
+
+ diff.dispatchUpdatesTo(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt
new file mode 100644
index 00000000..651f0df9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt
@@ -0,0 +1,95 @@
+package ru.melod1n.project.vkm.adapter.diffutil
+
+import androidx.recyclerview.widget.DiffUtil
+import ru.melod1n.project.vkm.api.model.VKConversation
+
+class ConversationsCallback(
+ private val oldList: List,
+ private val newList: List
+) : DiffUtil.Callback() {
+
+ companion object {
+ const val DATE = "date"
+ const val ONLINE = "online"
+ const val AVATAR = "avatar"
+ const val USER_AVATAR = "user_avatar"
+ const val ATTACHMENTS = "attachments"
+ const val READ = "read"
+ const val NOTIFICATIONS = "notifications"
+ const val EDIT_MESSAGE = "edit_message"
+ const val MESSAGE = "message"
+ const val USER = "user"
+ const val GROUP = "group"
+ const val CONVERSATION = "conversation"
+ }
+
+ override fun getOldListSize() = oldList.size
+
+ override fun getNewListSize() = newList.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val old = oldList[oldItemPosition]
+ val new = newList[newItemPosition]
+
+ if (true) return false
+ return old.conversationId == new.conversationId
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val old = oldList[oldItemPosition]
+ val new = newList[newItemPosition]
+
+ val oldMessage = old.lastMessage
+ val newMessage = new.lastMessage
+
+ if (true) return false else
+
+ return old.title == new.title &&
+ old.lastMessageId == new.lastMessageId &&
+ old.photo50 == new.photo50 &&
+ old.unreadCount == new.unreadCount &&
+
+ old.isNoSound == new.isNoSound &&
+ old.isDisabledForever == new.isDisabledForever &&
+ old.disabledUntil == new.disabledUntil &&
+
+ old.inRead == new.inRead &&
+ old.outRead == new.outRead &&
+
+ old.peerUser == new.peerUser &&
+ old.peerGroup == new.peerGroup &&
+
+ oldMessage == newMessage
+
+// oldMessage.messageId == newMessage.messageId &&
+// oldMessage.isOut == newMessage.isOut &&
+// oldMessage.fromId == newMessage.fromId &&
+// oldMessage.date == newMessage.date &&
+// oldMessage.action == newMessage.action &&
+// oldMessage.text == newMessage.text &&
+// oldMessage.attachments == newMessage.attachments &&
+// oldMessage.fwdMessages == newMessage.fwdMessages &&
+// oldMessage.messageId == newMessage.messageId &&
+//
+// oldMessage.fromUser == newMessage.fromUser &&
+// oldMessage.fromGroup == newMessage.fromGroup
+ }
+
+ override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
+ val oldConversation = oldList[oldItemPosition]
+ val newConversation = newList[newItemPosition]
+
+ val oldMessage = oldConversation.lastMessage
+ val newMessage = newConversation.lastMessage
+
+ val oldDate = oldMessage.date
+ val newDate = newMessage.date
+
+// if (oldDate != newDate) return DATE
+
+// if (oldMessage != newMessage) return MESSAGE
+
+ return super.getChangePayload(oldItemPosition, newItemPosition)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt
new file mode 100644
index 00000000..9c18fcf9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt
@@ -0,0 +1,50 @@
+package ru.melod1n.project.vkm.adapter.diffutil
+
+import androidx.recyclerview.widget.DiffUtil
+import ru.melod1n.project.vkm.api.model.VKUser
+
+class UsersCallback(private val oldList: List, private val newList: List) : DiffUtil.Callback() {
+
+ companion object {
+ const val ONLINE = "online"
+ const val ONLINE_MOBILE = "online_mobile"
+ }
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val old = oldList[oldItemPosition]
+ val new = newList[newItemPosition]
+
+ return old.userId == new.userId
+ }
+
+ override fun getOldListSize() = oldList.size
+
+ override fun getNewListSize() = newList.size
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val old = oldList[oldItemPosition]
+ val new = newList[newItemPosition]
+
+ return old.firstName == new.firstName &&
+ old.lastName == new.lastName &&
+ old.isOnline == new.isOnline &&
+ old.isOnlineMobile == new.isOnlineMobile &&
+ old.lastSeen == new.lastSeen &&
+ old.lastSeenPlatform == new.lastSeenPlatform &&
+ old.deactivated == new.deactivated
+ }
+
+ override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
+ val old = oldList[oldItemPosition]
+ val new = newList[newItemPosition]
+
+ if (old.isOnlineMobile != new.isOnlineMobile) {
+ if (old.isOnline != new.isOnline) return ONLINE
+
+ return ONLINE_MOBILE
+ }
+
+ return super.getChangePayload(oldItemPosition, newItemPosition)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt b/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt
new file mode 100644
index 00000000..2b516b6f
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt
@@ -0,0 +1,42 @@
+package ru.melod1n.project.vkm.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
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt b/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt
new file mode 100644
index 00000000..3738a6e9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt
@@ -0,0 +1,41 @@
+package ru.melod1n.project.vkm.api
+
+import android.text.TextUtils
+import ru.melod1n.project.vkm.common.AppGlobal
+
+object UserConfig {
+
+ private const val TOKEN = "token"
+ private const val USER_ID = "user_id"
+
+ const val API_ID = "6964679"
+
+ var token = ""
+ var userId = 0
+
+ fun save() {
+ AppGlobal.preferences.edit()
+ .putString(TOKEN, token)
+ .putInt(USER_ID, userId)
+ .apply()
+ }
+
+ fun restore() {
+ token = AppGlobal.preferences.getString(TOKEN, "")!!
+ userId = AppGlobal.preferences.getInt(USER_ID, -1)
+ }
+
+ fun clear() {
+ token = ""
+ userId = -1
+
+ AppGlobal.preferences.edit()
+ .remove(TOKEN)
+ .remove(USER_ID)
+ .apply()
+ }
+
+ fun isLoggedIn(): Boolean {
+ return userId > 0 && !TextUtils.isEmpty(token)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt
new file mode 100644
index 00000000..4d0a0018
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt
@@ -0,0 +1,504 @@
+package ru.melod1n.project.vkm.api
+
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.annotation.WorkerThread
+import org.json.JSONArray
+import org.json.JSONObject
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.activity.DropUserDataActivity
+import ru.melod1n.project.vkm.api.method.MessageMethodSetter
+import ru.melod1n.project.vkm.api.method.MethodSetter
+import ru.melod1n.project.vkm.api.method.UserMethodSetter
+import ru.melod1n.project.vkm.api.model.*
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.net.HttpRequest
+import java.util.*
+import kotlin.collections.ArrayList
+
+@Suppress("UNCHECKED_CAST")
+object VKApi {
+
+ private var context: Context? = null
+
+ private const val TAG = "VKM:VKApi"
+
+ const val BASE_URL = "https://api.vk.com/method/"
+
+ const val API_VERSION = "5.132"
+
+ val language: String = AppGlobal.locale.language
+
+ fun init(context: Context) {
+ this.context = context
+ }
+
+ @WorkerThread
+ @Suppress("UNCHECKED_CAST")
+ fun execute(url: String, cls: Class?): ArrayList? {
+ if (BuildConfig.DEBUG) {
+ Log.w(TAG, "url: $url")
+ }
+
+ val buffer = 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
+
+ VKLongPollServer::class.java -> {
+ json.optJSONObject("response")?.let {
+ return arrayListOf(VKLongPollServer(it)) as ArrayList?
+ }
+ }
+
+ Boolean::class.java -> {
+ val value = json.optInt("response") == 1
+ return arrayListOf(value) as ArrayList?
+ }
+
+ Long::class.java -> {
+ val value = json.optLong("response")
+ return arrayListOf(value) as ArrayList?
+ }
+
+ Int::class.java -> {
+ val value = json.optInt("response")
+ return arrayListOf(value) as ArrayList?
+ }
+ }
+
+ val response = json.opt("response") ?: return null
+
+ val array = optItems(json) ?: return null
+ val models = ArrayList(array.length())
+
+ when (cls) {
+ VKUser::class.java -> {
+ json.optJSONObject("response")?.let { r ->
+ VKUser.friendsCount = r.optInt("count")
+ }
+
+ for (i in 0 until array.length()) {
+ models.add(VKUser(array.optJSONObject(i)) as T)
+ }
+ }
+
+ VKMessage::class.java -> {
+ response as JSONObject
+
+ if (url.contains("messages.getHistory")) {
+ VKMessage.lastHistoryCount = response.optInt("count")
+
+ response.optJSONArray("profiles")?.let {
+ val profiles = arrayListOf()
+
+ for (j in 0 until it.length()) {
+ profiles.add(VKUser(it.optJSONObject(j)))
+ }
+
+ VKMessage.profiles = profiles
+ }
+
+ response.optJSONArray("groups")?.let {
+ val groups = arrayListOf()
+
+ for (j in 0 until it.length()) {
+ groups.add(VKGroup(it.optJSONObject(j)))
+ }
+
+ VKMessage.groups = groups
+ }
+
+ response.optJSONArray("conversations")?.let {
+ val conversations = arrayListOf()
+
+ for (j in 0 until it.length()) {
+ conversations.add(VKConversation(it.optJSONObject(j)))
+ }
+
+ 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 = VKMessage(source)
+ models.add(message as T)
+ }
+
+ val profiles = ArrayList()
+ response.optJSONArray("profiles")?.let {
+ profiles.addAll(VKUser.parse(it))
+ }
+
+ val groups = ArrayList()
+ response.optJSONArray("groups")?.let {
+ groups.addAll(VKGroup.parse(it))
+ }
+
+ AppGlobal.database.let {
+ it.users.insert(profiles)
+ it.groups.insert(groups)
+ }
+ }
+
+ VKGroup::class.java -> {
+ for (i in 0 until array.length()) {
+ models.add(VKGroup(array.optJSONObject(i)) as T)
+ }
+ }
+
+ VKModel::class.java -> {
+ if (url.contains("messages.getHistoryAttachments")) {
+ return VKAttachments.parse(array) as ArrayList
+ }
+ }
+
+ VKConversation::class.java -> {
+ if (url.contains("getConversationsById")) {
+ for (i in 0 until array.length()) {
+ val source = array.optJSONObject(i)
+ models.add(VKConversation(source) as T)
+ }
+
+ return models
+ }
+
+ json.optJSONObject("response")?.let { r ->
+ 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 = VKConversation(oConversation).also {
+ it.lastMessage = VKMessage(oLastMessage)
+ }
+
+ response.optJSONArray("profiles")?.let {
+ val profiles = arrayListOf()
+
+ for (j in 0 until it.length()) {
+ profiles.add(VKUser(it.optJSONObject(j)))
+ }
+
+ VKConversation.profiles = profiles
+ }
+
+ response.optJSONArray("groups")?.let {
+ val groups = arrayListOf()
+
+ for (j in 0 until it.length()) {
+ groups.add(VKGroup(it.optJSONObject(j)))
+ }
+
+ VKConversation.groups = groups
+ }
+
+ models.add(conversation as T)
+ }
+ }
+ }
+
+ return models
+ }
+
+ fun execute(url: String, cls: Class, listener: OnResponseListener?) {
+ TaskManager.execute {
+ try {
+ val models = execute(url, cls)
+
+ listener?.let {
+ AppGlobal.handler.post(SuccessCallback(listener, models as E))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+
+ listener?.let {
+ AppGlobal.handler.post(ErrorCallback(listener, e))
+ }
+ }
+ }
+ }
+
+ fun executeArray(url: String, cls: Class, listener: OnResponseListener>?) {
+ TaskManager.execute {
+ try {
+ val models = execute(url, cls)
+
+ listener?.let {
+ AppGlobal.handler.post(SuccessArrayCallback(listener, models as ArrayList))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+
+ listener?.let {
+ AppGlobal.handler.post(ErrorCallback(listener, 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)
+
+ 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(
+ private val listener: OnResponseListener?,
+ private val response: E
+ ) : Runnable {
+ override fun run() {
+ listener?.onResponse(response)
+ }
+ }
+
+ class SuccessArrayCallback(
+ private val listener: OnResponseListener>?,
+ private val response: ArrayList
+ ) : Runnable {
+ override fun run() {
+ listener?.onResponse(response)
+ }
+ }
+
+ class ErrorCallback(
+ private val listener: OnResponseListener?,
+ private val exception: Exception
+ ) : Runnable {
+ override fun run() {
+ listener?.onError(exception)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt
new file mode 100644
index 00000000..6c4f7a13
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt
@@ -0,0 +1,15 @@
+package ru.melod1n.project.vkm.api
+
+enum class VKApiKeys {
+ READ_MESSAGE,
+ RESTORE_MESSAGE,
+ UPDATE_MESSAGE,
+ NEW_MESSAGE,
+ EDIT_MESSAGE,
+ DELETE_MESSAGE,
+ UPDATE_CONVERSATION,
+
+ UPDATE_USER,
+
+ UPDATE_GROUP
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt
new file mode 100644
index 00000000..0e0b7442
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt
@@ -0,0 +1,59 @@
+package ru.melod1n.project.vkm.api
+
+import android.util.Log
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.api.util.VKUtil
+import java.net.URLEncoder
+
+object VKAuth {
+
+ private const val TAG = "VKM.VKAuth"
+
+ var redirectUrl = "https://oauth.vk.com/blank.html"
+
+ fun getDirectAuthUrl(login: String, password: String, captcha: String = ""): String {
+ 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")}"
+ }
+
+ const val settings =
+ "notify,friends,photos,audio,video,docs,status,notes,pages,wall,groups,messages,offline,notifications"
+
+ @Throws(Exception::class)
+ fun parseRedirectUrl(url: String): Array {
+ val accessToken = VKUtil.extractPattern(url, "access_token=(.*?)&")
+ val userId = VKUtil.extractPattern(url, "user_id=(\\d*)")
+
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "access_token=$accessToken")
+ Log.i(TAG, "user_id=$userId")
+ }
+
+ if (userId == null || userId.isEmpty() || accessToken == null || accessToken.isEmpty()) throw Exception(
+ "Failed to parse redirect url $url"
+ )
+
+ return arrayOf(accessToken, userId)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt
new file mode 100644
index 00000000..65ccba46
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt
@@ -0,0 +1,14 @@
+package ru.melod1n.project.vkm.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"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt
new file mode 100644
index 00000000..ca0dc097
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt
@@ -0,0 +1,146 @@
+package ru.melod1n.project.vkm.api
+
+import android.util.Log
+import androidx.annotation.WorkerThread
+import org.json.JSONArray
+import ru.melod1n.project.vkm.api.model.VKMessage
+import ru.melod1n.project.vkm.api.util.VKUtil
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.event.EventInfo
+
+@Suppress("UNCHECKED_CAST")
+class VKLongPollParser {
+
+ companion object {
+
+ @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)
+ }
+ }
+ }
+
+ 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, message)
+
+ sendEvent(info)
+ }
+ }
+
+ private fun messageEdit(item: JSONArray) {
+ val message = VKUtil.parseLongPollMessage(item)
+ val info = EventInfo(VKApiKeys.EDIT_MESSAGE, 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, arrayOf(peerId, messageId))
+
+ MemoryCache.deleteMessage(messageId)
+
+ sendEvent(info)
+ }
+
+ private fun messageRestored(item: JSONArray) {
+ val message = VKUtil.parseLongPollMessage(item)
+ val info = EventInfo(VKApiKeys.RESTORE_MESSAGE, 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, 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 onReadMessage(messageId: Int, peerId: Int)
+ fun onDeleteMessage(messageId: Int, peerId: Int)
+ fun onRestoredMessage(message: VKMessage)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt
new file mode 100644
index 00000000..06f56c43
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt
@@ -0,0 +1,205 @@
+package ru.melod1n.project.vkm.api.method
+
+import ru.melod1n.project.vkm.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): 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", 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): MessageMethodSetter {
+ put("attachment", ArrayUtils.asString(attachments))
+ return this
+ }
+
+ fun attachment(vararg attachments: String): MessageMethodSetter {
+ put("attachment", ArrayUtils.asString(*attachments))
+ return this
+ }
+
+ fun forwardMessages(ids: Collection): MessageMethodSetter {
+ put("forward_messages", ArrayUtils.asString(ids))
+ return this
+ }
+
+ fun forwardMessages(vararg ids: Int): MessageMethodSetter {
+ put("forward_messages", 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", ArrayUtils.asString(ids))
+ return this
+ }
+
+ fun chatIds(ids: Collection): MessageMethodSetter {
+ put("max_msg_id", 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt
new file mode 100644
index 00000000..0c7405e5
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt
@@ -0,0 +1,166 @@
+package ru.melod1n.project.vkm.api.method
+
+import android.util.ArrayMap
+import android.util.Log
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApi
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.util.ArrayUtils
+import java.net.URLEncoder
+
+@Suppress("UNCHECKED_CAST")
+open class MethodSetter(private val name: String) {
+
+ private val params: ArrayMap = 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"] = UserConfig.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
+ }
+
+ fun execute(cls: Class): ArrayList? {
+ return VKApi.execute(getSignedUrl(), cls)
+ }
+
+ fun executeArray(cls: Class, listener: OnResponseListener>?) {
+ VKApi.executeArray(getSignedUrl(), cls, listener)
+ }
+
+ fun execute(cls: Class, listener: OnResponseListener?) {
+ 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", ArrayUtils.asString(ids))
+ }
+
+ fun userIds(ids: ArrayList): MethodSetter {
+ return put("user_ids", 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", ArrayUtils.asString(ids))
+ }
+
+ fun groupIds(ids: ArrayList): MethodSetter {
+ return put("group_ids", 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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt
new file mode 100644
index 00000000..83137e71
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt
@@ -0,0 +1,44 @@
+package ru.melod1n.project.vkm.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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt
new file mode 100644
index 00000000..02205b99
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt
@@ -0,0 +1,59 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONArray
+import java.util.*
+
+object VKAttachments {
+
+ private const val TYPE_PHOTO = "photo"
+ private const val TYPE_VIDEO = "video"
+ private const val TYPE_AUDIO = "audio"
+ private const val TYPE_DOC = "doc"
+ private const val TYPE_LINK = "link"
+ private const val TYPE_STICKER = "sticker"
+ private const val TYPE_GIFT = "gift"
+ private const val TYPE_AUDIO_MESSAGE = "audio_message"
+ private const val TYPE_GRAFFITI = "graffiti"
+ private const val TYPE_POLL = "poll"
+ private const val TYPE_GEO = "geo"
+ private const val TYPE_WALL = "wall"
+ private const val TYPE_CALL = "call"
+ private const val TYPE_STORY = "story"
+ private const val TYPE_POINT = "point"
+ private const val TYPE_MARKET = "market"
+ private const val TYPE_ARTICLE = "article"
+ private const val TYPE_PODCAST = "podcast"
+ private const val TYPE_WALL_REPLY = "wall_reply"
+ private const val TYPE_MONEY_REQUEST = "money_request"
+ private const val TYPE_AUDIO_PLAYLIST = "audio_playlist"
+
+ fun parse(array: JSONArray): ArrayList {
+ val attachments = ArrayList(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 = attachment.optString("type")
+ val jsonObject = attachment.optJSONObject(type) ?: continue
+
+ when (type) {
+ TYPE_PHOTO -> attachments.add(VKPhoto(jsonObject))
+ TYPE_AUDIO -> attachments.add(VKAudio(jsonObject))
+ TYPE_VIDEO -> attachments.add(VKVideo(jsonObject))
+ TYPE_DOC -> attachments.add(VKDoc(jsonObject))
+ TYPE_STICKER -> attachments.add(VKSticker(jsonObject))
+ TYPE_LINK -> attachments.add(VKLink(jsonObject))
+ TYPE_GIFT -> attachments.add(VKGift(jsonObject))
+ TYPE_AUDIO_MESSAGE -> attachments.add(VKAudioMessage(jsonObject))
+ TYPE_GRAFFITI -> attachments.add(VKGraffiti(jsonObject))
+ TYPE_POLL -> attachments.add(VKPoll(jsonObject))
+ TYPE_CALL -> attachments.add(VKCall(jsonObject))
+ }
+ }
+
+ return attachments
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt
new file mode 100644
index 00000000..dd44a9d6
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt
@@ -0,0 +1,17 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKAudio(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var id = o.optInt("id", -1)
+ var ownerId = o.optInt("owner_id", -1)
+ var artist: String = o.optString("artist")
+ var title: String = o.optString("title")
+ var duration = o.optInt("duration")
+ var url: String = o.optString("url")
+ var date = o.optInt("date")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt
new file mode 100644
index 00000000..03e3c877
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt
@@ -0,0 +1,23 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKAudioMessage(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var duration = o.optInt("duration")
+ var waveform = ArrayList()
+ var linkOgg: String = o.optString("link_ogg")
+ var linkMp3: String = o.optString("link_mp3")
+
+ init {
+ o.optJSONArray("waveform")?.let {
+ val waveform = ArrayList()
+ for (i in 0 until it.length()) {
+ waveform.add(it.optInt(i))
+ }
+ this.waveform = waveform
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt
new file mode 100644
index 00000000..d43294fb
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt
@@ -0,0 +1,15 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKCall(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var initiatorId = o.optInt("initiator_id", -1)
+ var receiverId = o.optInt("receiver_id", -1)
+ var state: String = o.optString("state") //reached, canceled_by_initiator, canceled_by_receiver
+ var time = o.optInt("time")
+ var duration = o.optInt("duration")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt
new file mode 100644
index 00000000..84b0464b
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt
@@ -0,0 +1,4 @@
+package ru.melod1n.project.vkm.api.model
+
+class VKComment { //https://vk.com/dev/objects/comment
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt
new file mode 100644
index 00000000..c449e5a5
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt
@@ -0,0 +1,144 @@
+package ru.melod1n.project.vkm.api.model
+
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.RoomWarnings
+import org.json.JSONObject
+
+@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
+@Entity(tableName = "conversations")
+class VKConversation() : VKModel(), Cloneable {
+
+ companion object {
+ var profiles = arrayListOf()
+ var groups = arrayListOf()
+
+ var conversationsCount: Int = 0
+
+ const val STATE_IN = "in"
+ const val STATE_KICKED = "kicked"
+ const val STATE_LEFT = "left"
+
+ const val TYPE_USER = "user"
+ const val TYPE_CHAT = "chat"
+ const val TYPE_GROUP = "group"
+
+ var count = 0
+ }
+
+ /*
+ 18 — пользователь заблокирован или удален;
+ 900 — нельзя отправить сообщение пользователю, который в чёрном списке;
+ 901 — пользователь запретил сообщения от сообщества;
+ 902 — пользователь запретил присылать ему сообщения с помощью настроек приватности;
+ 915 — в сообществе отключены сообщения;
+ 916 — в сообществе заблокированы сообщения;
+ 917 — нет доступа к чату;
+ 918 — нет доступа к e-mail;
+ 203 — нет доступа к сообществу
+ */
+
+ var isAllowed = false
+ var reason = -1
+
+ var inRead = 0
+ var outRead = 0
+ var lastMessageId = 0
+ var unreadCount = 0
+
+ @PrimaryKey(autoGenerate = false)
+ var conversationId = 0
+
+ var type: String = ""
+ var localId = 0
+
+ var disabledUntil = 0
+ var isDisabledForever = false
+ var isNoSound = false
+
+ var membersCount = 0
+ var title: String = ""
+
+ var pinnedMessageId = 0
+
+ var state: String = ""
+
+ @Embedded(prefix = "cMessage")
+ var lastMessage = VKMessage()
+
+ var isGroupChannel = false
+
+ var photo50: String = ""
+ var photo100: String = ""
+ var photo200: String = ""
+
+ @Embedded(prefix = "cUser")
+ var peerUser: VKUser? = null
+
+ @Embedded(prefix = "cGroup")
+ var peerGroup: VKGroup? = null
+
+ constructor(o: JSONObject) : this() {
+ inRead = o.optInt("in_read")
+ outRead = o.optInt("out_read")
+ lastMessageId = o.optInt("last_message_id", -1)
+ unreadCount = o.optInt("unread_count", 0)
+
+ o.optJSONObject("peer")?.let {
+ conversationId = it.optInt("id", -1)
+ type = 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")
+ reason = it.optInt("reason", -1)
+ }
+
+ o.optJSONObject("chat_settings")?.let {
+ membersCount = it.optInt("members_count")
+ title = it.optString("title")
+
+ it.optJSONObject("pinned_message")?.let { pinned ->
+ pinnedMessageId = VKPinnedMessage(pinned).id
+ }
+
+ state = 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 isChatId() = conversationId > 2_000_000_000
+
+ fun isChat() = type == TYPE_CHAT
+
+ fun isUser() = type == TYPE_USER
+
+ fun isNotUser() = !isUser()
+
+ fun isGroup() = type == TYPE_GROUP
+
+ override fun toString(): String {
+ return title
+ }
+
+ public override fun clone(): VKConversation {
+ return super.clone() as VKConversation
+ }
+}
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt
new file mode 100644
index 00000000..9c87a05b
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt
@@ -0,0 +1,73 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+import java.util.*
+
+class VKDoc(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ companion object {
+ const val TYPE_NONE = 0
+ const val TYPE_TEXT = 1
+ const val TYPE_ARCHIVE = 2
+ const val TYPE_GIF = 3
+ const val TYPE_IMAGE = 4
+ const val TYPE_AUDIO = 5
+ const val TYPE_VIDEO = 6
+ const val TYPE_BOOK = 7
+ const val TYPE_UNKNOWN = 8
+ }
+
+ var id = o.optInt("id", -1)
+ var ownerId = o.optInt("owner_id", -1)
+ var title: String = o.optString("title")
+ var size = o.optInt("size")
+ var ext: String = o.optString("ext")
+ var url: String = o.optString("url")
+ var date = o.optInt("date")
+ var type = o.optInt("type")
+ var preview: Preview? = null
+
+ init {
+ o.optJSONObject("preview")?.let {
+ preview = Preview(it)
+ }
+ }
+
+ class Preview(o: JSONObject) {
+ var photo: Photo? = null
+ var graffiti: Graffiti? = null
+
+ inner class Photo(o: JSONObject) {
+ var sizes: ArrayList? = null
+
+ init {
+ o.optJSONArray("sizes")?.let {
+ val sizes = ArrayList()
+ for (i in 0 until it.length()) {
+ sizes.add(VKPhotoSize(it.optJSONObject(i)))
+ }
+ this.sizes = sizes
+ }
+ }
+ }
+
+ class Graffiti(o: JSONObject) {
+ var src: String = o.optString("src")
+ var width = o.optInt("width")
+ var height = o.optInt("height")
+ }
+
+ init {
+ o.optJSONObject("photo")?.let {
+ photo = Photo(it)
+ }
+
+ o.optJSONObject("graffiti")?.let {
+ graffiti = Graffiti(it)
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt
new file mode 100644
index 00000000..67f7aa72
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt
@@ -0,0 +1,18 @@
+package ru.melod1n.project.vkm.api.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "friends")
+class VKFriend() {
+
+ @PrimaryKey(autoGenerate = false)
+ var friendId: Int = -1
+
+ var userId: Int = -1
+
+ constructor(friendId: Int, userId: Int): this() {
+ this.friendId = friendId
+ this.userId = userId
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt
new file mode 100644
index 00000000..9226b7fc
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt
@@ -0,0 +1,14 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKGift(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var id = o.optInt("id", -1)
+ var thumb256: String = o.optString("thumb_256")
+ var thumb96: String = o.optString("thumb_96")
+ var thumb48: String = o.optString("thumb_48")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt
new file mode 100644
index 00000000..aebfbd6d
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt
@@ -0,0 +1,16 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKGraffiti(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var id = o.optInt("id", -1)
+ var ownerId = o.optInt("owner_id", -1)
+ var url: String = o.optString("url")
+ var width = o.optInt("width")
+ var height = o.optInt("height")
+ var accessKey: String = o.optString("access_key")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt
new file mode 100644
index 00000000..e65b657a
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt
@@ -0,0 +1,46 @@
+package ru.melod1n.project.vkm.api.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import org.json.JSONArray
+import org.json.JSONObject
+
+@Entity(tableName = "groups")
+open class VKGroup(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ companion object {
+ const val DEFAULT_FIELDS = "description,members_count,counters,status,verified"
+
+ fun isGroupId(id: Int): Boolean {
+ return id < 0
+ }
+
+ val EMPTY: VKGroup = object : VKGroup() {
+ init {
+ name = "Unknown"
+ }
+ }
+
+ fun parse(array: JSONArray): ArrayList {
+ val groups = ArrayList()
+
+ for (i in 0 until array.length()) {
+ groups.add(VKGroup(array.optJSONObject(i)))
+ }
+ return groups
+ }
+ }
+
+ @PrimaryKey(autoGenerate = false)
+ var groupId = o.optInt("id", -1)
+ var name: String = o.optString("name")
+ var screenName: String = o.optString("screen_name")
+ var isClosed = o.optInt("is_closed") == 1
+ var deactivated: String = o.optString("deactivated")
+ var type: String = o.optString("type")
+ var photo50: String = o.optString("photo_50")
+ var photo100: String = o.optString("photo_100")
+ var photo200: String = o.optString("photo_200")
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt
new file mode 100644
index 00000000..debc463d
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt
@@ -0,0 +1,46 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+import java.io.Serializable
+
+class VKLink(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var url: String = o.optString("url")
+ var title: String = o.optString("title")
+ var caption: String = o.optString("caption")
+ var description: String = o.optString("description")
+ var previewPage: String = o.optString("preview_page")
+ var previewUrl: String = o.optString("preview_url")
+ var photo: VKPhoto? = null
+ var button: Button? = null
+
+ init {
+ 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")
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt
new file mode 100644
index 00000000..1ee940d2
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt
@@ -0,0 +1,12 @@
+package ru.melod1n.project.vkm.api.model
+
+import java.util.*
+
+class VKLongPollHistory : VKModel() {
+
+ private val lpMessages: ArrayList? = null
+ private val messages: ArrayList? = null
+ private val profiles: ArrayList? = null
+ private val groups: ArrayList? = null //TODO: использовать
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt
new file mode 100644
index 00000000..cffc1834
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt
@@ -0,0 +1,13 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKLongPollServer(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var key: String = o.optString("key")
+ var server = o.optString("server").replace("\\", "")
+ var ts: Long = o.optLong("ts")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt
new file mode 100644
index 00000000..66392ea8
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt
@@ -0,0 +1,4 @@
+package ru.melod1n.project.vkm.api.model
+
+class VKMarketAlbum { //https://vk.com/dev/objects/market_album
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt
new file mode 100644
index 00000000..04986485
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt
@@ -0,0 +1,4 @@
+package ru.melod1n.project.vkm.api.model
+
+class VKMarketItem { //https://vk.com/dev/objects/market_item
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt
new file mode 100644
index 00000000..3f63df31
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt
@@ -0,0 +1,177 @@
+package ru.melod1n.project.vkm.api.model
+
+import android.util.ArrayMap
+import androidx.room.*
+import org.json.JSONObject
+import ru.melod1n.project.vkm.database.dao.converters.ArrayListToByteArrayConverter
+import ru.melod1n.project.vkm.database.dao.converters.ForwardedConverter
+
+@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
+@Entity(tableName = "messages")
+open class VKMessage() : VKModel() {
+
+ companion object {
+
+ var profiles = arrayListOf()
+ var groups = arrayListOf()
+ var conversations = arrayListOf()
+
+ const val serialVersionUID = 1L
+
+ var lastHistoryCount = 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()
+
+ 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
+ }
+ }
+
+ @PrimaryKey(autoGenerate = false)
+ var messageId = 0
+ var date = 0
+ var peerId = 0
+ var fromId = 0
+ var editTime = 0
+ var isOut = false
+ var text: String = ""
+ var randomId = 0
+ var conversationMessageId = 0
+
+ var hasEmoji = false
+ var isImportant = false
+ var isRead = false
+
+ @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
+ @TypeConverters(ArrayListToByteArrayConverter::class)
+ var attachments: ArrayList = arrayListOf()
+
+ // @Ignore
+ @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
+ @TypeConverters(ForwardedConverter::class)
+ var fwdMessages: ArrayList = arrayListOf()
+
+ var replyMessageId = 0
+
+ @Embedded(prefix = "mAction")
+ var action: VKMessageAction? = null
+
+ @Embedded(prefix = "mUser")
+ var fromUser: VKUser? = null
+
+ @Embedded(prefix = "Group")
+ var fromGroup: VKGroup? = null
+
+ constructor(o: JSONObject) : this() {
+ messageId = 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 = 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(it.length())
+ for (i in 0 until it.length()) {
+ fwdMessages.add(VKMessage(it.optJSONObject(i)))
+ }
+ this.fwdMessages = fwdMessages
+ }
+
+ o.optJSONObject("reply_message")?.let {
+ replyMessageId = VKMessage(it).messageId
+ }
+
+ o.optJSONObject("action")?.let {
+ action = VKMessageAction(it)
+ }
+ }
+
+ fun getForwardedMessages() = ArrayList().apply {
+ for (model in fwdMessages) add(model as VKMessage)
+ }
+
+ fun isFromUser() = fromId > 0
+
+ fun isFromGroup() = fromId < 0
+
+ fun isOutbox() = isOut
+
+ fun isInbox() = !isOutbox()
+
+ fun isReaded() = isRead
+
+ fun isUnreaded() = !isReaded()
+
+ override fun toString(): String {
+ return if (text.isNotEmpty()) {
+ text
+ } else {
+ super.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt
new file mode 100644
index 00000000..154c9ad3
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt
@@ -0,0 +1,57 @@
+package ru.melod1n.project.vkm.api.model
+
+import androidx.room.Ignore
+import org.json.JSONObject
+
+class VKMessageAction() : VKModel() {
+
+ companion object {
+ 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"
+ }
+
+ /*
+ chat_photo_update — обновлена фотография беседы;
+ chat_photo_remove — удалена фотография беседы;
+ chat_create — создана беседа;
+ chat_title_update — обновлено название беседы;
+ chat_invite_user — приглашен пользователь;
+ chat_kick_user — исключен пользователь;
+ chat_pin_message — закреплено сообщение;
+ chat_unpin_message — откреплено сообщение;
+ chat_invite_user_by_link — пользователь присоединился к беседе по ссылке.
+ */
+
+ var type: String = ""
+
+ var memberId = 0
+
+ @Ignore
+ var message: VKMessage? = null
+
+ var conversationMessageId = 0
+
+ var text: String = ""
+ var oldText: String = ""
+
+// @Embedded(prefix = "photo")
+// var photo: Photo? = null
+
+ //TODO: add photo
+
+ constructor(o: JSONObject) : this() {
+ type = o.optString("type")
+ memberId = o.optInt("member_id", -1)
+ text = o.optString("text")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt
new file mode 100644
index 00000000..ecdfdaa2
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt
@@ -0,0 +1,9 @@
+package ru.melod1n.project.vkm.api.model
+
+import java.io.Serializable
+
+abstract class VKModel : Serializable {
+ companion object {
+ const val serialVersionUID = 1L
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt
new file mode 100644
index 00000000..9ceff1d5
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt
@@ -0,0 +1,28 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+import java.util.*
+
+class VKPhoto(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var id = o.optInt("id", -1)
+ var albumId = o.optInt("album_id", -1)
+ var ownerId = o.optInt("owner_id", -1)
+ var text: String = o.optString("text")
+ var date = o.optInt("date")
+ var width = o.optInt("width")
+ var height = o.optInt("height")
+ var sizes: ArrayList? = null
+
+ init {
+ o.optJSONArray("sizes")?.let {
+ val sizes = ArrayList()
+ for (i in 0 until it.length()) {
+ sizes.add(VKPhotoSize(it.optJSONObject(i)))
+ }
+ this.sizes = sizes
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt
new file mode 100644
index 00000000..ce1d23f0
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt
@@ -0,0 +1,14 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKPhotoSize(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var type: String = o.optString("type")
+ var url: String = o.optString("url")
+ var height = o.optInt("height")
+ var width = o.optInt("width")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt
new file mode 100644
index 00000000..c61176ec
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt
@@ -0,0 +1,25 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+import java.util.*
+
+class VKPinnedMessage(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var id = o.optInt("id", -1)
+ var date = o.optInt("date")
+ var fromId = o.optInt("from_id", -1)
+ var text: String = o.optString("text")
+ var attachments: ArrayList? = null
+ var fwdMessages: ArrayList? = null
+
+ init {
+ o.optJSONArray("attachments")?.let {
+ attachments = VKAttachments.parse(it)
+ }
+
+ //TODO: parse forwarded
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt
new file mode 100644
index 00000000..9ba066d4
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt
@@ -0,0 +1,55 @@
+package ru.melod1n.project.vkm.api.model
+
+import android.graphics.Color
+import org.json.JSONObject
+import java.io.Serializable
+import java.util.*
+
+class VKPoll(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ 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()
+ var isAnonymous = o.optBoolean("anonymous")
+ var isMultiple = o.optBoolean("multiple")
+ var answerIds = ArrayList()
+ 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()
+ 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")
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt
new file mode 100644
index 00000000..4dcadd73
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt
@@ -0,0 +1,31 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+import java.util.*
+
+class VKSticker(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ var productId = o.optInt("product_id", -1)
+ var stickerId = o.optInt("sticker_id", -1)
+ var images: ArrayList? = null
+
+ init {
+ o.optJSONArray("images")?.let {
+ val images = ArrayList()
+ for (i in 0 until it.length()) {
+ images.add(Image(it.optJSONObject(i)))
+ }
+ this.images = images
+ }
+ }
+
+ class Image(o: JSONObject) : VKModel() {
+
+ var url: String = o.optString("url")
+ var width = o.optInt("width")
+ var height = o.optInt("height")
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt
new file mode 100644
index 00000000..37918ae3
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt
@@ -0,0 +1,74 @@
+package ru.melod1n.project.vkm.api.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import org.json.JSONArray
+import org.json.JSONObject
+
+@Entity(tableName = "users")
+open class VKUser(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ companion object {
+
+ var friendsCount: Int = 0
+
+ const val DEFAULT_FIELDS =
+ "photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
+
+ val EMPTY: VKUser = object : VKUser() {
+ override fun toString(): String {
+ return "Unknown Unknown"
+ }
+ }
+
+ fun isUserId(id: Int): Boolean {
+ return id > 0 && id < 2000000000
+ }
+
+
+ @JvmStatic
+ fun parse(array: JSONArray): ArrayList {
+ val users = ArrayList()
+
+ for (i in 0 until array.length()) {
+ users.add(VKUser(array.optJSONObject(i)))
+ }
+
+ return users
+ }
+ }
+
+ @PrimaryKey(autoGenerate = false)
+ var userId = o.optInt("id", -1)
+ var firstName: String = o.optString("first_name")
+ var lastName: String = o.optString("last_name")
+ var deactivated: String = o.optString("deactivated")
+ var isClosed = o.optBoolean("is_closed")
+ var isCanAccessClosed = o.optBoolean("can_access_closed")
+ var sex = o.optInt("sex")
+ var screenName: String = o.optString("screen_name")
+ var photo50: String = o.optString("photo_50")
+ var photo100: String = o.optString("photo_100")
+ var photo200: String = o.optString("photo_200")
+ var isOnline = o.optInt("online") == 1
+ var isOnlineMobile = isOnline && o.optInt("online_mobile") == 1
+ var status: String = o.optString("status")
+
+ var lastSeen = 0
+ var lastSeenPlatform = 0
+
+ var isVerified = o.optInt("verified") == 1
+
+ init {
+ o.optJSONObject("last_seen")?.let {
+ lastSeen = it.optInt("time")
+ lastSeenPlatform = it.optInt("platform")
+ }
+ }
+
+ override fun toString(): String {
+ return "$firstName $lastName"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt
new file mode 100644
index 00000000..48595690
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt
@@ -0,0 +1,37 @@
+package ru.melod1n.project.vkm.api.model
+
+import org.json.JSONObject
+
+class VKVideo(o: JSONObject) : VKModel() {
+
+ constructor() : this(JSONObject())
+
+ 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")
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt
new file mode 100644
index 00000000..0cc6a244
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt
@@ -0,0 +1,4 @@
+package ru.melod1n.project.vkm.api.model
+
+class VKWall { //https://vk.com/dev/objects/post
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt b/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt
new file mode 100644
index 00000000..0751c3ce
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt
@@ -0,0 +1,680 @@
+package ru.melod1n.project.vkm.api.util
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.content.ContextCompat
+import com.amulyakhare.textdrawable.TextDrawable
+import org.json.JSONArray
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.model.*
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.common.TaskManager
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.extensions.ContextExtensions.color
+import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable
+import ru.melod1n.project.vkm.extensions.StringExtensions.lowerCase
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import ru.melod1n.project.vkm.util.TextUtils
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.regex.Pattern
+import kotlin.math.abs
+
+object VKUtil {
+
+ private const val TAG = "VKM: 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,
+ firstOnTop: Boolean
+ ): ArrayList {
+ 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,
+ firstOnTop: Boolean
+ ): ArrayList {
+ 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 matchMentions(text: String): String {
+ return text
+ }
+
+// 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
+// }
+
+ fun getUserOnline(user: VKUser): String {
+ val r = AppGlobal.resources
+ return if (user.isOnline) {
+ if (user.isOnlineMobile) {
+ r.getString(R.string.user_online_mobile)
+ } else {
+ r.getString(R.string.user_online)
+ }
+ } else {
+ if (user.lastSeen == 0) {
+ r.getString(R.string.user_last_seen_recently)
+ } else {
+ r.getString(R.string.user_last_seen_at, getLastSeenTime(user.lastSeen * 1000L))
+ }
+ }
+ }
+
+ fun getUserOnlineIcon(
+ context: Context,
+ conversation: VKConversation?,
+ peerUser: VKUser?
+ ): Drawable? {
+ return if (conversation != null) {
+ if (conversation.isUser() && peerUser != null) {
+ if (!peerUser.isOnline) {
+ null
+ } else {
+ ContextCompat.getDrawable(
+ context,
+ if (peerUser.isOnlineMobile) R.drawable.ic_online_mobile else R.drawable.ic_online_pc
+ )
+ }
+ } else null
+ } else {
+ if (peerUser!!.isOnline) {
+ ContextCompat.getDrawable(
+ context,
+ if (peerUser.isOnlineMobile) R.drawable.ic_online_mobile else R.drawable.ic_online_pc
+ )
+ } else {
+ null
+ }
+ }
+ }
+
+ fun getUserOnlineIcon(context: Context, user: VKUser): Drawable? {
+ return getUserOnlineIcon(context, null, user)
+ }
+
+ //TODO: нормальное время
+ fun getLastSeenTime(date: Long): String {
+ return SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
+ }
+
+ fun getAvatarPlaceholder(context: Context, dialogTitle: String): TextDrawable {
+ return TextDrawable.builder().buildRound(
+ if (dialogTitle.isEmpty()) "" else {
+ TextUtils.getFirstLetterFromString(dialogTitle)
+ },
+ context.color(R.color.accent)
+ )
+ }
+
+ @WorkerThread
+ fun searchUser(id: Int, onResponseListener: OnResponseListener? = null): VKUser? {
+ return if (VKGroup.isGroupId(id) || isChatId(id)) {
+ null
+ } else {
+ MemoryCache.getUserById(id)?.let { return it }
+
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "User with id $id not found")
+ }
+
+ TaskManager.loadUser(VKApiKeys.UPDATE_USER, id, onResponseListener)
+
+ return null
+ }
+ }
+
+ @WorkerThread
+ fun searchGroup(id: Int, onResponseListener: OnResponseListener? = null): VKGroup? {
+ return if (!VKGroup.isGroupId(id) || isChatId(id)) {
+ null
+ } else {
+ MemoryCache.getGroupById(abs(id))?.let { return it }
+
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Group with id $id not found")
+ }
+
+ TaskManager.loadGroup(VKApiKeys.UPDATE_GROUP, abs(id), onResponseListener)
+
+ return null
+ }
+ }
+
+ 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 getDialogType(context: Context, conversation: VKConversation): Drawable? {
+ return when {
+ conversation.isGroupChannel -> {
+ ContextCompat.getDrawable(context, R.drawable.ic_dialog_type_channel)
+ }
+ conversation.isChat() -> {
+ ContextCompat.getDrawable(context, R.drawable.ic_dialog_type_conversation)
+ }
+ else -> null
+ }
+ }
+
+ fun getAttachmentText(context: Context, attachments: List): String {
+ val resId: Int
+
+ if (attachments.isNotEmpty()) {
+ if (attachments.size > 1) {
+ var oneType = true
+ val className = attachments[0].javaClass.simpleName
+
+ for (model in attachments) {
+ if (model.javaClass.simpleName != className) {
+ oneType = false
+ break
+ }
+ }
+
+ return if (oneType) {
+ val objectClass: Class = attachments[0].javaClass
+ resId = when (objectClass) {
+ VKPhoto::class.java -> {
+ R.string.message_attachment_photos
+ }
+ VKVideo::class.java -> {
+ R.string.message_attachment_videos
+ }
+ VKAudio::class.java -> {
+ R.string.message_attachment_audios
+ }
+ VKDoc::class.java -> {
+ R.string.message_attachment_docs
+ }
+ else -> {
+ -1
+ }
+ }
+ if (resId == -1) "Unknown attachments" else context.getString(
+ resId,
+ attachments.size
+ ).toLowerCase(Locale.getDefault())
+ } else {
+ context.getString(R.string.message_attachments_many)
+ }
+ } else {
+ val objectClass: Class = attachments[0].javaClass
+
+ resId = when (objectClass) {
+ VKPhoto::class.java -> {
+ R.string.message_attachment_photo
+ }
+ VKAudio::class.java -> {
+ R.string.message_attachment_audio
+ }
+ VKVideo::class.java -> {
+ R.string.message_attachment_video
+ }
+ VKDoc::class.java -> {
+ R.string.message_attachment_doc
+ }
+ VKGraffiti::class.java -> {
+ R.string.message_attachment_graffiti
+ }
+ VKAudioMessage::class.java -> {
+ R.string.message_attachment_voice
+ }
+ VKSticker::class.java -> {
+ R.string.message_attachment_sticker
+ }
+ VKGift::class.java -> {
+ R.string.message_attachment_gift
+ }
+ VKLink::class.java -> {
+ R.string.message_attachment_link
+ }
+ VKPoll::class.java -> {
+ R.string.message_attachment_poll
+ }
+ VKCall::class.java -> {
+ R.string.message_attachment_call
+ }
+ else -> {
+ return "Unknown"
+ }
+ }
+ }
+ } else {
+ return ""
+ }
+ return context.getString(resId)
+ }
+
+ fun getAttachmentDrawable(context: Context, attachments: List): Drawable? {
+ if (attachments.isEmpty() || attachments.size > 1) return null
+
+ var resId = -1
+
+ when (attachments[0].javaClass) {
+ VKPhoto::class.java -> {
+ resId = R.drawable.ic_message_attachment_camera
+ }
+ VKAudio::class.java -> {
+ resId = R.drawable.ic_message_attachment_audio
+ }
+ VKVideo::class.java -> {
+ resId = R.drawable.ic_message_attachment_video
+ }
+ VKDoc::class.java -> {
+ resId = R.drawable.ic_message_attachment_doc
+ }
+ VKGraffiti::class.java -> {
+ resId = R.drawable.ic_message_attachment_graffiti
+ }
+ VKAudioMessage::class.java -> {
+ resId = R.drawable.ic_message_attachment_audio_message
+ }
+ VKSticker::class.java -> {
+ resId = R.drawable.ic_message_attachment_sticker
+ }
+ VKGift::class.java -> {
+ resId = R.drawable.ic_message_attachment_gift
+ }
+ VKLink::class.java -> {
+ resId = R.drawable.ic_message_attachment_link
+ }
+ VKPoll::class.java -> {
+ resId = R.drawable.ic_message_attachment_poll
+ }
+ VKCall::class.java -> {
+ resId = R.drawable.ic_message_attachment_call
+ }
+ }
+
+ if (resId != -1) {
+ val drawable = context.drawable(resId)
+
+ drawable?.setTint(context.color(R.color.accent))
+ return drawable
+ }
+ return null
+ }
+
+ fun getFwdText(context: Context, forwardedMessages: List): String {
+ return if (forwardedMessages.isNotEmpty()) {
+ if (forwardedMessages.size > 1) {
+ context.getString(R.string.message_fwd_many, forwardedMessages.size).lowerCase()
+ } else {
+ context.getString(R.string.message_fwd_one)
+ }
+ } else ""
+ }
+
+ @Deprecated("need to rewrite")
+ fun getActionText(
+ context: Context,
+ lastMessage: VKMessage,
+ onResponseListener: OnResponseListener
+ ) {
+ TaskManager.execute {
+ lastMessage.action?.let {
+ var result = ""
+
+ when (it.type) {
+ VKMessageAction.ACTION_CHAT_CREATE -> result = context.getString(
+ R.string.message_action_created_chat,
+ ""
+ )
+ VKMessageAction.ACTION_INVITE_USER -> result =
+ if (lastMessage.fromId == lastMessage.action!!.memberId) {
+ context.getString(R.string.message_action_returned_to_chat, "")
+ } else {
+ val invited = MemoryCache.getUserById(lastMessage.action!!.memberId)
+ context.getString(R.string.message_action_invited_user, invited)
+ }
+ VKMessageAction.ACTION_INVITE_USER_BY_LINK -> result = context.getString(
+ R.string.message_action_invited_by_link,
+ ""
+ )
+ VKMessageAction.ACTION_KICK_USER -> result =
+ if (lastMessage.fromId == lastMessage.action!!.memberId) {
+ context.getString(R.string.message_action_left_from_chat, "")
+ } else {
+ val kicked = MemoryCache.getUserById(lastMessage.action!!.memberId)
+ context.getString(R.string.message_action_kicked_user, kicked)
+ }
+ VKMessageAction.ACTION_PHOTO_REMOVE -> result = context.getString(
+ R.string.message_action_removed_photo,
+ ""
+ )
+ VKMessageAction.ACTION_PHOTO_UPDATE -> result = context.getString(
+ R.string.message_action_updated_photo,
+ ""
+ )
+ VKMessageAction.ACTION_PIN_MESSAGE -> result = context.getString(
+ R.string.message_action_pinned_message,
+ ""
+ )
+ VKMessageAction.ACTION_UNPIN_MESSAGE -> result = context.getString(
+ R.string.message_action_unpinned_message,
+ ""
+ )
+ VKMessageAction.ACTION_TITLE_UPDATE -> result = context.getString(
+ R.string.message_action_updated_title,
+ ""
+ )
+ }
+
+ AppGlobal.post { onResponseListener.onResponse(result) }
+ }
+ }
+ }
+
+ fun getTime(context: Context, lastMessage: VKMessage): String {
+ val then = lastMessage.date * 1000L
+ val now = System.currentTimeMillis()
+
+ val change = abs(now - then)
+
+ val seconds = change / 1000
+
+ if (seconds == 0L) {
+ return context.getString(R.string.time_format_now)
+ }
+
+ val minutes = seconds / 60
+
+ if (minutes == 0L) {
+ return context.getString(R.string.time_format_second, seconds)
+ }
+
+ val hours = minutes / 60
+
+ if (hours == 0L) {
+ return context.getString(R.string.time_format_minute, minutes)
+ }
+
+ val days = hours / 24
+
+ if (days == 0L) {
+ return context.getString(R.string.time_format_hour, hours)
+ }
+
+ val months = days / 30
+
+ if (months == 0L) {
+ return context.getString(R.string.time_format_day, days)
+ }
+
+ val years = months / 12
+
+ if (years == 0L) {
+ return context.getString(R.string.time_format_month, months)
+ } else if (years > 0L) {
+ return context.getString(R.string.time_format_year, years)
+ }
+
+ return SimpleDateFormat("HH:mm", Locale.getDefault()).format(then)
+ }
+
+ fun parseConversations(array: JSONArray): ArrayList {
+ val conversations = arrayListOf()
+ for (i in 0 until array.length()) {
+ conversations.add(VKConversation(array.optJSONObject(i)))
+ }
+
+ return conversations
+ }
+
+ fun parseMessages(array: JSONArray): ArrayList {
+ val messages = arrayListOf()
+ for (i in 0 until array.length()) {
+ messages.add(VKMessage(array.optJSONObject(i)))
+ }
+
+ return messages
+ }
+
+ fun isChatId(id: Int) = id > 2_000_000_000
+
+ 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.messageId = id
+ message.peerId = peerId
+ message.date = date
+ message.text = text
+
+ val fromId =
+ if (isMessageHasFlag(flags, "outbox")) UserConfig.userId
+ else peerId
+
+ message.fromId = fromId
+
+ 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 = it.optString("source_act")
+
+ when (action.type) {
+ VKMessageAction.ACTION_CHAT_CREATE -> {
+ action.text = it.optString("source_text")
+ }
+ VKMessageAction.ACTION_TITLE_UPDATE -> {
+ action.oldText = it.optString("source_old_text")
+ action.text = it.optString("source_text")
+ }
+ VKMessageAction.ACTION_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.ACTION_UNPIN_MESSAGE -> {
+ action.memberId = it.optInt("source_mid")
+ action.conversationMessageId = it.optInt("source_chat_local_id")
+ }
+ VKMessageAction.ACTION_INVITE_USER,
+ VKMessageAction.ACTION_KICK_USER,
+ VKMessageAction.ACTION_SCREENSHOT,
+ VKMessageAction.ACTION_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 == UserConfig.userId
+ message.isOut = out
+
+ if (message.isFromUser()) {
+ message.fromUser = MemoryCache.getUserById(fromId)
+ } else {
+ message.fromGroup = MemoryCache.getGroupById(abs(fromId))
+ }
+
+ return message
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt
new file mode 100644
index 00000000..e0aa3937
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt
@@ -0,0 +1,12 @@
+package ru.melod1n.project.vkm.base
+
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+
+abstract class BaseActivity : AppCompatActivity() {
+
+ fun getRootView(): View {
+ return findViewById(android.R.id.content)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt
new file mode 100644
index 00000000..55cc2b89
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt
@@ -0,0 +1,175 @@
+package ru.melod1n.project.vkm.base
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import androidx.recyclerview.widget.RecyclerView
+import ru.melod1n.project.vkm.common.AppGlobal
+import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList
+import ru.melod1n.project.vkm.listener.ItemClickListener
+import ru.melod1n.project.vkm.listener.ItemLongClickListener
+import java.io.Serializable
+import java.util.*
+
+
+@Suppress("UNCHECKED_CAST")
+abstract class BaseAdapter(
+ var context: Context,
+ var values: ArrayList = arrayListOf()
+) : RecyclerView.Adapter() {
+
+ companion object {
+ private const val P_ITEMS = "BaseAdapter.values"
+ }
+
+ private var cleanValues: ArrayList? = null
+
+ private var inflater: LayoutInflater = LayoutInflater.from(context)
+
+ var itemClickListener: ItemClickListener? = null
+ var itemLongClickListener: ItemLongClickListener? = null
+
+ open fun destroy() {}
+
+ open fun getItem(position: Int): T {
+ return values[position]
+ }
+
+ fun add(position: Int, item: T) {
+ values.add(position, item)
+ cleanValues?.add(position, item)
+ }
+
+ fun add(item: T) {
+ values.add(item)
+ cleanValues?.add(item)
+ }
+
+ fun addAll(items: List) {
+ values.addAll(items)
+ cleanValues?.addAll(items)
+ }
+
+ fun addAll(position: Int, items: List) {
+ values.addAll(position, items)
+ cleanValues?.addAll(position, items)
+ }
+
+ operator fun set(position: Int, item: T) {
+ values[position] = item
+ cleanValues?.set(position, item)
+ }
+
+ fun indexOf(item: T): Int {
+ return values.indexOf(item)
+ }
+
+ fun removeAt(index: Int) {
+ values.removeAt(index)
+ cleanValues?.removeAt(index)
+ }
+
+ fun remove(item: T) {
+ values.remove(item)
+ cleanValues?.remove(item)
+ }
+
+ fun isEmpty() = values.isNullOrEmpty()
+
+ fun isNotEmpty() = !isEmpty()
+
+ fun view(resId: Int, viewGroup: ViewGroup): View {
+ return inflater.inflate(resId, viewGroup, false)
+ }
+
+ fun updateValues(arrayList: ArrayList) {
+ values.clear()
+ values.addAll(arrayList)
+ }
+
+ fun updateValues(list: List) = updateValues(list.asArrayList())
+
+ override fun onBindViewHolder(holder: VH, position: Int) {
+ onBindItemViewHolder(holder, position)
+ }
+
+ protected fun initListeners(itemView: View, position: Int) {
+ if (itemView is AdapterView<*>) return
+
+ itemView.setOnClickListener {
+ if (itemClickListener != null) itemClickListener!!.onItemClick(
+ position
+ )
+ }
+ itemView.setOnLongClickListener {
+ if (itemLongClickListener != null) itemLongClickListener!!.onItemLongClick(position)
+ itemClickListener == null
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return values.size
+ }
+
+ private fun onBindItemViewHolder(holder: VH, position: Int) {
+ initListeners(holder.itemView, position)
+ holder.bind(position)
+ }
+
+ fun onSaveInstanceState(): Parcelable {
+ val bundle = Bundle()
+ if (values.size > 0 && (values[0] is Parcelable || values[0] is Serializable)) {
+ bundle.putSerializable(P_ITEMS, values)
+ }
+ return bundle
+ }
+
+ fun post(runnable: Runnable) {
+ AppGlobal.handler.post(runnable)
+ }
+
+ fun onRestoreInstanceState(state: Parcelable?) {
+ if (state is Bundle) {
+ if (state.containsKey(P_ITEMS)) {
+ values = state.getSerializable(P_ITEMS) as ArrayList
+ }
+ }
+ }
+
+ fun clear() {
+ values.clear()
+ }
+
+ open fun filter(query: String) {
+ if (cleanValues == null) {
+ cleanValues = ArrayList(values)
+ }
+
+ values.clear()
+
+ if (query.isEmpty()) {
+ values.addAll(cleanValues!!)
+ } else {
+ for (item in cleanValues!!) {
+ if (onQueryItem(item, query)) {
+ values.add(item)
+ }
+ }
+ }
+
+ notifyDataSetChanged()
+ }
+
+ open fun onQueryItem(item: T, query: String): Boolean {
+ return false
+ }
+
+ operator fun get(index: Int): T {
+ return values[index]
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt
new file mode 100644
index 00000000..5e5e8b7c
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt
@@ -0,0 +1,21 @@
+package ru.melod1n.project.vkm.base
+
+import androidx.annotation.IdRes
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.Fragment
+import ru.melod1n.project.vkm.activity.MainActivity
+
+abstract class BaseFragment : Fragment() {
+
+ protected open fun initToolbar(@IdRes resId: Int) {
+ val toolbar: Toolbar = requireView().findViewById(resId)
+
+ activity?.let {
+ if (it is MainActivity && toolbar is ru.melod1n.project.vkm.widget.Toolbar) it.initToolbar(toolbar)
+ }
+ }
+
+ protected fun runOnUi(runnable: Runnable) {
+ activity?.runOnUiThread(runnable)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt
new file mode 100644
index 00000000..4728256c
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt
@@ -0,0 +1,33 @@
+package ru.melod1n.project.vkm.base
+
+import android.os.Bundle
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.fragment.app.DialogFragment
+import ru.melod1n.project.vkm.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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt
new file mode 100644
index 00000000..c54a55b4
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt
@@ -0,0 +1,8 @@
+package ru.melod1n.project.vkm.base
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
+ abstract fun bind(position: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt
new file mode 100644
index 00000000..80574680
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt
@@ -0,0 +1,21 @@
+package ru.melod1n.project.vkm.base.mvp
+
+import android.app.Application
+import android.os.Handler
+
+object MvpBase {
+
+ lateinit var handler: Handler
+
+ fun init(application: Application) {
+ handler = Handler(application.mainLooper)
+ }
+
+ fun init(appHandler: Handler) {
+ handler = appHandler
+ }
+
+ fun post(runnable: Runnable) {
+ handler.post(runnable)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt
new file mode 100644
index 00000000..b3286ffe
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt
@@ -0,0 +1,11 @@
+package ru.melod1n.project.vkm.base.mvp
+
+object MvpConstants {
+
+ const val PEER_ID = "_peer_id"
+ const val ID = "_id"
+ const val COUNT = "_count"
+ const val OFFSET = "_offset"
+ const val FROM_CACHE = "_from_cache"
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt
new file mode 100644
index 00000000..8cb241a1
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt
@@ -0,0 +1,3 @@
+package ru.melod1n.project.vkm.base.mvp
+
+class MvpException(var errorId: String) : Exception(errorId)
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt
new file mode 100644
index 00000000..42feda41
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt
@@ -0,0 +1,30 @@
+package ru.melod1n.project.vkm.base.mvp
+
+import androidx.collection.arrayMapOf
+import java.util.*
+
+@Suppress("UNCHECKED_CAST")
+class MvpFields {
+ private val fields = arrayMapOf()
+
+ fun put(key: String, value: Any): MvpFields {
+ fields[key] = value
+ return this
+ }
+
+ operator fun get(key: String): T {
+ return fields[key] as T
+ }
+
+ fun getNonNull(key: String): Any {
+ return fields[key]!!
+ }
+
+ fun getNonNull(`object`: Any): Any {
+ return Objects.requireNonNull(`object`)
+ }
+
+ fun getFields(): Map {
+ return fields
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt
new file mode 100644
index 00000000..82033808
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt
@@ -0,0 +1,9 @@
+package ru.melod1n.project.vkm.base.mvp
+
+interface MvpOnLoadListener {
+
+ fun onResponse(response: T)
+
+ fun onError(t: Throwable)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt
new file mode 100644
index 00000000..20905db4
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt
@@ -0,0 +1,106 @@
+package ru.melod1n.project.vkm.base.mvp
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.NonNull
+import androidx.annotation.Nullable
+
+@Suppress("UNCHECKED_CAST")
+abstract class MvpPresenter, V : MvpView>(
+ protected var viewState: V,
+ private val repositoryStringClassName: String
+) {
+
+ protected var context: Context? = null
+
+ protected fun requireContext(): Context {
+ if (context == null) throw IllegalStateException("context is null")
+
+ return context!!
+ }
+
+ enum class ListState {
+ EMPTY, EMPTY_LOADING, EMPTY_NO_INTERNET, EMPTY_ERROR, FILLED, FILLED_LOADING
+ }
+
+ protected var tag: String = ""
+
+ lateinit var repository: Repository
+
+ init {
+ initRepository()
+ }
+
+ private fun initRepository() {
+ val clazz = Class.forName(repositoryStringClassName)
+
+ this.repository = clazz.newInstance() as Repository
+ }
+
+ open fun onCreate(context: Context, bundle: Bundle? = null) {
+ this.context = context
+ }
+
+ open fun onCreateView(bundle: Bundle? = null) {}
+ open fun onViewCreated(bundle: Bundle? = null) {}
+
+ protected fun post(runnable: Runnable) {
+ MvpBase.post(runnable)
+ }
+
+ open fun destroy() {}
+
+ fun prepareViews() {
+ viewState.prepareNoItemsView()
+ viewState.prepareNoInternetView()
+ viewState.prepareErrorView()
+ }
+
+ fun setState(state: ListState) {
+ when (state) {
+ ListState.EMPTY -> {
+ viewState.hideRefreshLayout()
+ viewState.hideProgressBar()
+ viewState.showNoItemsView()
+ viewState.hideNoInternetView()
+ viewState.hideErrorView()
+ }
+ ListState.EMPTY_LOADING -> {
+ viewState.hideRefreshLayout()
+ viewState.showProgressBar()
+ viewState.hideNoItemsView()
+ viewState.hideNoInternetView()
+ viewState.hideErrorView()
+ }
+ ListState.EMPTY_NO_INTERNET -> {
+ viewState.hideRefreshLayout()
+ viewState.hideProgressBar()
+ viewState.hideNoItemsView()
+ viewState.showNoInternetView()
+ viewState.hideErrorView()
+ }
+ ListState.EMPTY_ERROR -> {
+ viewState.hideRefreshLayout()
+ viewState.hideProgressBar()
+ viewState.hideNoItemsView()
+ viewState.hideNoInternetView()
+ viewState.showErrorView()
+ }
+ ListState.FILLED -> {
+ viewState.hideRefreshLayout()
+ viewState.hideProgressBar()
+ viewState.hideNoItemsView()
+ viewState.hideNoInternetView()
+ viewState.hideErrorView()
+ }
+ ListState.FILLED_LOADING -> {
+ viewState.showRefreshLayout()
+ viewState.hideProgressBar()
+ viewState.hideNoItemsView()
+ viewState.hideNoInternetView()
+ viewState.hideErrorView()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt
new file mode 100644
index 00000000..9b6dbeb9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt
@@ -0,0 +1,48 @@
+package ru.melod1n.project.vkm.base.mvp
+
+@Suppress("UNCHECKED_CAST")
+abstract class MvpRepository {
+
+ // abstract fun loadValues(fields: MvpFields, listener: MvpOnLoadListener?)
+//
+// abstract fun loadCachedValues(fields: MvpFields, listener: MvpOnLoadListener?)
+//
+// protected fun sendError(listener: MvpOnLoadListener?, errorId: String) {
+// listener?.onErrorLoad(MvpException(errorId))
+// }
+//
+ protected fun - sendError(
+ listener: MvpOnLoadListener
- ,
+ t: Throwable
+ ) {
+// if (listener !is MvpOnLoadListener) return
+
+ MvpBase.post { listener.onError(t) }
+ }
+
+ protected fun
- sendResponseArray(
+ listener: MvpOnLoadListener>,
+ response: ArrayList
-
+ ) {
+ listener.let { MvpBase.handler.post { listener.onResponse(response) } }
+ }
+
+ protected fun
- sendResponse(
+ listener: MvpOnLoadListener
- ,
+ response: Item
+ ) {
+ listener.let {
+ MvpBase.handler.post { listener.onResponse(response) }
+ }
+ }
+
+// protected open fun cacheLoadedValues(values: ArrayList) {}
+//
+// protected fun startNewThread(runnable: Runnable) {
+// Thread(runnable).start()
+// }
+//
+// protected fun post(runnable: Runnable) {
+// MvpBase.handler.post(runnable)
+// }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt
new file mode 100644
index 00000000..485fb1cc
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt
@@ -0,0 +1,33 @@
+package ru.melod1n.project.vkm.base.mvp
+
+interface MvpView {
+
+ fun showErrorSnackbar(t: Throwable)
+
+ fun prepareNoItemsView()
+
+ fun showNoItemsView()
+
+ fun hideNoItemsView()
+
+ fun prepareNoInternetView()
+
+ fun showNoInternetView()
+
+ fun hideNoInternetView()
+
+ fun prepareErrorView()
+
+ fun showErrorView()
+
+ fun hideErrorView()
+
+ fun showProgressBar()
+
+ fun hideProgressBar()
+
+ fun showRefreshLayout()
+
+ fun hideRefreshLayout()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt b/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt
new file mode 100644
index 00000000..2bfd4f02
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt
@@ -0,0 +1,163 @@
+package ru.melod1n.project.vkm.common
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.app.DownloadManager
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.net.ConnectivityManager
+import android.os.Handler
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.content.pm.PackageInfoCompat
+import androidx.preference.PreferenceManager
+import androidx.room.Room
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.microsoft.appcenter.AppCenter
+import com.microsoft.appcenter.analytics.Analytics
+import com.microsoft.appcenter.crashes.Crashes
+import org.acra.ACRA
+import org.acra.ReportingInteractionMode
+import org.acra.annotation.ReportsCrashes
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.api.UserConfig
+import ru.melod1n.project.vkm.api.VKApi
+import ru.melod1n.project.vkm.base.mvp.MvpBase
+import ru.melod1n.project.vkm.database.AppDatabase
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.fragment.FragmentSettings
+import ru.melod1n.project.vkm.util.AndroidUtils
+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
+)
+class AppGlobal : Application() {
+
+ companion object {
+ const val APP_CENTER_TOKEN = "c87e410a-d622-4c52-ad7e-7388ab511704"
+
+ lateinit var windowManager: WindowManager
+ lateinit var connectivityManager: ConnectivityManager
+ lateinit var inputMethodManager: InputMethodManager
+ lateinit var clipboardManager: ClipboardManager
+ lateinit var downloadManager: DownloadManager
+
+ lateinit var preferences: SharedPreferences
+ lateinit var locale: Locale
+ lateinit var handler: Handler
+ lateinit var resources: Resources
+ lateinit var packageName: String
+ lateinit var database: AppDatabase
+ lateinit var instance: AppGlobal
+
+ 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) {
+ AppCenter.start(
+ this, APP_CENTER_TOKEN, Analytics::class.java, Crashes::class.java
+ )
+
+ ACRA.init(this)
+ }
+
+ Fresco.initialize(this)
+
+ database = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
+ .fallbackToDestructiveMigration()
+ .build()
+
+ preferences = PreferenceManager.getDefaultSharedPreferences(this)
+ handler = Handler(mainLooper)
+ locale = Locale.getDefault()
+
+ 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
+
+ inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+
+ screenWidth = AndroidUtils.getDisplayWidth()
+ screenHeight = AndroidUtils.getDisplayHeight()
+
+ UserConfig.restore()
+
+ VKApi.init(this)
+
+ MvpBase.init(handler)
+
+ fillMemoryCache()
+
+ applyNightMode()
+ }
+
+ private fun fillMemoryCache() {
+ TaskManager.execute {
+ val users = database.users.getAll()
+ val groups = database.groups.getAll()
+
+ MemoryCache.appendUsers(users)
+ MemoryCache.appendGroups(groups)
+ }
+ }
+
+ fun applyNightMode(value: String? = null) {
+ val mode = value ?: preferences.getString(FragmentSettings.KEY_THEME, "-1")!!
+
+ val nightMode = getNightMode(mode.toInt())
+
+ val oldNightMode = AppCompatDelegate.getDefaultNightMode()
+
+ AppCompatDelegate.setDefaultNightMode(nightMode)
+ }
+
+ fun getNightMode(nightMode: Int = -1): Int {
+ val mode = if (nightMode != -1) nightMode else preferences.getString(
+ FragmentSettings.KEY_THEME,
+ "-1"
+ )!!.toInt()
+
+ return when (mode) {
+ 1 -> AppCompatDelegate.MODE_NIGHT_YES
+ 2 -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
+ 3 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ else -> AppCompatDelegate.MODE_NIGHT_NO
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt b/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt
new file mode 100644
index 00000000..de40b341
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt
@@ -0,0 +1,118 @@
+package ru.melod1n.project.vkm.common
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import ru.melod1n.project.vkm.R
+import ru.melod1n.project.vkm.fragment.FragmentConversations
+import ru.melod1n.project.vkm.fragment.FragmentFriends
+import ru.melod1n.project.vkm.fragment.FragmentImportant
+
+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
+ ) {
+ 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) {
+ fragmentToShow = createFragmentByTag(tag)
+ transaction.add(containerId, fragmentToShow, tag)
+ } 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()
+ }
+
+ private fun createFragmentByTag(tag: String): Fragment {
+ return when (tag) {
+ "FragmentFriends" -> FragmentFriends()
+ "FragmentImportant" -> FragmentImportant()
+ "FragmentConversations" -> FragmentConversations()
+ else -> Fragment()
+ }
+ }
+}
diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt
new file mode 100644
index 00000000..d18a4fc2
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt
@@ -0,0 +1,363 @@
+package ru.melod1n.project.vkm.common
+
+import android.util.Log
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.api.VKApi
+import ru.melod1n.project.vkm.api.VKApiKeys
+import ru.melod1n.project.vkm.api.method.MethodSetter
+import ru.melod1n.project.vkm.api.model.VKConversation
+import ru.melod1n.project.vkm.api.model.VKGroup
+import ru.melod1n.project.vkm.api.model.VKMessage
+import ru.melod1n.project.vkm.api.model.VKUser
+import ru.melod1n.project.vkm.concurrent.LowThread
+import ru.melod1n.project.vkm.database.MemoryCache
+import ru.melod1n.project.vkm.event.EventInfo
+import ru.melod1n.project.vkm.listener.OnResponseListener
+import java.util.*
+import java.util.stream.Collectors
+
+object TaskManager {
+
+ private const val TAG = "TaskManager"
+
+ private val groupsTasksIds = arrayListOf()
+
+ private var groupsTimer: Timer? = null
+
+ private val usersTasksIds = arrayListOf()
+
+ private var usersTimer: Timer? = null
+
+ private val messagesTasksIds = arrayListOf()
+
+ private val messagesReadIds = arrayListOf()
+
+ private var messagesReadTimer: Timer? = null
+
+ private val conversationsTasksIds = arrayListOf()
+
+ private val listeners = arrayListOf()
+
+ fun addOnEventListener(listener: OnEventListener?) {
+ listeners.add(listener)
+ }
+
+ fun removeOnEventListener(listener: OnEventListener?) {
+ listeners.remove(listener)
+ }
+
+ fun execute(runnable: Runnable?) {
+ LowThread(runnable).start()
+ }
+
+ private fun addProcedure(
+ methodSetter: MethodSetter,
+ className: Class,
+ pushInfo: EventInfo<*>?,
+ responseListener: OnResponseListener?
+ ) {
+ execute {
+ methodSetter.executeArray(className, object : OnResponseListener> {
+ override fun onResponse(response: ArrayList) {
+ if (response.isEmpty()) return
+
+ responseListener?.onResponse(response[0])
+
+ pushInfo?.let { sendEvent(it) }
+ }
+
+ override fun onError(t: Throwable) {
+ responseListener?.onError(t)
+ }
+
+ })
+ }
+ }
+
+ fun sendEvent(eventInfo: EventInfo<*>) {
+ AppGlobal.handler.post {
+ for (listener in listeners) {
+ listener?.onNewEvent(eventInfo)
+ }
+ }
+ }
+
+ fun loadUser(
+ eventKey: VKApiKeys,
+ userId: Int,
+ responseListener: OnResponseListener? = null
+ ) {
+ if (usersTasksIds.contains(userId)) return
+ usersTasksIds.add(userId)
+
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Load user: $userId")
+ }
+
+ if (usersTimer != null) {
+ usersTimer?.cancel()
+ }
+
+ usersTimer = Timer()
+ usersTimer?.schedule(object : TimerTask() {
+ override fun run() {
+ val setter = VKApi.users()
+ .get()
+ .userIds(usersTasksIds)
+ .fields(VKUser.DEFAULT_FIELDS)
+
+ val usersIds = arrayListOf()
+ usersTasksIds.forEach { usersIds.add(it.toString()) }
+
+ addProcedure(
+ setter,
+ VKUser::class.java,
+ EventInfo(eventKey, usersTasksIds),
+ object : OnResponseListener {
+ override fun onResponse(response: VKUser) {
+ Log.d(
+ TAG,
+ "Loaded users: ${
+ usersIds.stream().collect(Collectors.joining(", "))
+ }"
+ )
+
+ usersTasksIds.remove(userId)
+ responseListener?.onResponse(response)
+
+ execute { MemoryCache.put(response) }
+ }
+
+ override fun onError(t: Throwable) {
+ Log.w(
+ TAG,
+ "Loaded users: ${
+ usersIds.stream().collect(Collectors.joining(", "))
+ }\nStack:${Log.getStackTraceString(t)}"
+ )
+ responseListener?.onError(t)
+ }
+
+ })
+
+ usersTimer = null
+ }
+ }, 500)
+ }
+
+ fun loadGroup(
+ eventKey: VKApiKeys,
+ groupId: Int,
+ responseListener: OnResponseListener? = null
+ ) {
+ if (groupsTasksIds.contains(groupId)) return
+ groupsTasksIds.add(groupId)
+
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Load group: $groupId")
+ }
+
+ val setter = VKApi.groups().getById()
+ .groupIds(groupsTasksIds)
+ .fields(VKGroup.DEFAULT_FIELDS)
+
+ if (groupsTimer != null) {
+ groupsTimer?.cancel()
+ }
+
+ groupsTimer = Timer()
+ groupsTimer?.schedule(object : TimerTask() {
+ override fun run() {
+ val groupsIds = arrayListOf()
+ groupsTasksIds.forEach { groupsIds.add(it.toString()) }
+
+ addProcedure(
+ setter,
+ VKGroup::class.java,
+ EventInfo(eventKey, groupsTasksIds),
+ object : OnResponseListener {
+ override fun onResponse(response: VKGroup) {
+ Log.d(
+ TAG,
+ "Loaded groups: ${
+ groupsIds.stream().collect(Collectors.joining(", "))
+ }"
+ )
+
+ groupsTasksIds.remove(groupId)
+ responseListener?.onResponse(response)
+
+ execute { MemoryCache.put(response) }
+ }
+
+ override fun onError(t: Throwable) {
+ Log.w(
+ TAG,
+ "Not loaded Group: ${
+ groupsIds.stream().collect(Collectors.joining(", "))
+ }\nStack: " + Log.getStackTraceString(
+ t
+ )
+ )
+ responseListener?.onError(t)
+ }
+ })
+ }
+ }, 500)
+
+ }
+
+ fun loadMessage(
+ eventKey: VKApiKeys,
+ messageId: Int,
+ responseListener: OnResponseListener? = null
+ ) {
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Load message: $messageId")
+ }
+ if (messagesTasksIds.contains(messageId)) return
+ messagesTasksIds.add(messageId)
+
+ val setter = VKApi.messages().getById()
+ .messageIds(messageId)
+ .extended(true)
+ .filter(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS)
+
+ addProcedure(
+ setter,
+ VKMessage::class.java,
+ EventInfo(eventKey, messageId),
+ object : OnResponseListener {
+ override fun onResponse(response: VKMessage) {
+ Log.d(TAG, "Loaded message: $messageId")
+
+ messagesTasksIds.remove(messageId)
+ responseListener?.onResponse(response)
+
+ execute { MemoryCache.put(response) }
+ }
+
+ override fun onError(t: Throwable) {
+ Log.w(
+ TAG,
+ "Not loaded message: $messageId. Stack: " + Log.getStackTraceString(t)
+ )
+ responseListener?.onError(t)
+ }
+ })
+ }
+
+ fun readMessage(
+ eventKey: VKApiKeys,
+ peerId: Int,
+ messageId: Int,
+ responseListener: OnResponseListener? = null
+ ) {
+ if (messagesReadIds.contains(messageId)) return
+ messagesReadIds.add(messageId)
+
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Read message: $messageId")
+ }
+
+ if (messagesReadTimer != null) {
+ messagesReadTimer?.cancel()
+ }
+
+ messagesReadTimer = Timer()
+ messagesReadTimer?.schedule(object : TimerTask() {
+ override fun run() {
+ val messagesIds = arrayListOf()
+ messagesReadIds.forEach { messagesIds.add(it.toString()) }
+
+ val setter = VKApi.messages().markAsRead()
+// .startMessageId(messageId)
+ .markConversationAsRead(true)
+ .peerId(peerId)
+
+ addProcedure(
+ setter,
+ Int::class.java,
+ EventInfo(eventKey, arrayOf(peerId, messageId)),
+ object : OnResponseListener {
+ override fun onResponse(response: Int) {
+ Log.d(
+ TAG,
+ "Readed messages: ${
+ messagesIds.stream().collect(Collectors.joining(", "))
+ }"
+ )
+
+ messagesReadIds.remove(messageId)
+ responseListener?.onResponse(response)
+
+ //TODO: update readed messages in cache
+// execute { MemoryCache.put(response) }
+ }
+
+ override fun onError(t: Throwable) {
+ Log.w(
+ TAG,
+ "Not readed messages: ${
+ messagesIds.stream().collect(Collectors.joining(", "))
+ }\nStack: " + Log.getStackTraceString(
+ t
+ )
+ )
+ responseListener?.onError(t)
+ }
+ }
+ )
+ }
+
+ }, 500)
+ }
+
+ fun loadConversation(
+ eventKey: VKApiKeys,
+ conversationId: Int,
+ responseListener: OnResponseListener? = null
+ ) {
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Load conversation: $conversationId")
+ }
+ if (conversationsTasksIds.contains(conversationId)) return
+ conversationsTasksIds.add(conversationId)
+
+ val setter = VKApi.messages()
+ .getConversationsById()
+ .peerIds(conversationId)
+ .extended(true)
+ .fields(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS)
+
+ addProcedure(
+ setter,
+ VKConversation::class.java,
+ EventInfo(eventKey, conversationId),
+ object : OnResponseListener {
+ override fun onResponse(response: VKConversation) {
+ Log.d(TAG, "Loaded conversation: $conversationId")
+
+ conversationsTasksIds.remove(conversationId)
+ responseListener?.onResponse(response)
+
+ execute { MemoryCache.put(response) }
+ }
+
+ override fun onError(t: Throwable) {
+ Log.w(
+ TAG,
+ "Not loaded conversation: $conversationId. Stack: " + Log.getStackTraceString(
+ t
+ )
+ )
+ responseListener?.onError(t)
+ }
+
+ })
+ }
+
+ interface OnEventListener {
+ fun onNewEvent(info: EventInfo<*>)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt
new file mode 100644
index 00000000..a782d901
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt
@@ -0,0 +1,98 @@
+package ru.melod1n.project.vkm.common
+
+import android.content.Context
+import android.content.IntentFilter
+import ru.melod1n.project.vkm.receiver.MinuteReceiver
+import java.util.*
+
+object TimeManager {
+
+ var currentHour = 0
+ var currentMinute = 0
+ var currentSecond = 0
+
+ private val onHourChangeListeners: ArrayList = ArrayList()
+ private val onMinuteChangeListeners: ArrayList = ArrayList()
+ private val onSecondChangeListeners: ArrayList = ArrayList()
+ private val onTimeChangeListeners: ArrayList = 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt
new file mode 100644
index 00000000..69f63008
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt
@@ -0,0 +1,80 @@
+package ru.melod1n.project.vkm.common
+
+import android.util.Log
+import androidx.collection.arrayMapOf
+import org.json.JSONArray
+import org.json.JSONObject
+import ru.melod1n.project.vkm.BuildConfig
+import ru.melod1n.project.vkm.model.NewUpdateInfo
+import ru.melod1n.project.vkm.net.HttpRequest
+
+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()
+ 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)
+// }
+// }
+// }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt b/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt
new file mode 100644
index 00000000..77a5b4f3
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt
@@ -0,0 +1,12 @@
+package ru.melod1n.project.vkm.concurrent
+
+import android.os.Process
+
+class LowThread(runnable: Runnable?) : Thread(runnable) {
+
+ override fun run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
+ super.run()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt b/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt
new file mode 100644
index 00000000..8194fc4d
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt
@@ -0,0 +1,20 @@
+package ru.melod1n.project.vkm.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import ru.melod1n.project.vkm.api.model.*
+import ru.melod1n.project.vkm.database.dao.*
+
+@Database(
+ entities = [VKConversation::class, VKMessage::class, VKUser::class, VKGroup::class, VKFriend::class],
+ version = 1,
+ exportSchema = false
+)
+
+abstract class AppDatabase : RoomDatabase() {
+ abstract val conversations: ConversationsDao
+ abstract val messages: MessagesDao
+ abstract val users: UsersDao
+ abstract val groups: GroupsDao
+ abstract val friends: FriendsDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt b/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt
new file mode 100644
index 00000000..08bb3ebc
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt
@@ -0,0 +1,176 @@
+package ru.melod1n.project.vkm.database
+
+import android.util.SparseArray
+import androidx.annotation.WorkerThread
+import ru.melod1n.project.vkm.api.model.*
+import ru.melod1n.project.vkm.common.AppGlobal
+
+object MemoryCache {
+
+ private val users = SparseArray()
+ private val groups = SparseArray()
+
+ @WorkerThread
+ fun getUserById(id: Int): VKUser? {
+ var user = users[id]
+ if (user == null) {
+ user = AppGlobal.database.users.getById(id)
+
+ user?.let { append(it) }
+ }
+ return user
+ }
+
+ @WorkerThread
+ fun getGroupById(positiveId: Int): VKGroup? {
+ var group = groups[positiveId]
+ if (group == null) {
+ group = AppGlobal.database.groups.getById(positiveId)
+
+ group?.let { append(it) }
+ }
+ return group
+ }
+
+ @WorkerThread
+ fun getMessageById(id: Int): VKMessage? {
+ return AppGlobal.database.messages.getById(id)
+ }
+
+ @WorkerThread
+ fun getMessagesByPeerId(peerId: Int): List {
+ return AppGlobal.database.messages.getByPeerId(peerId)
+ }
+
+ @WorkerThread
+ fun getMessages(): List {
+ return AppGlobal.database.messages.getAll()
+ }
+
+ @WorkerThread
+ fun getConversationById(id: Int): VKConversation? {
+ return AppGlobal.database.conversations.getById(id)
+ }
+
+ @WorkerThread
+ fun getConversations(): List {
+ return AppGlobal.database.conversations.getAll()
+ }
+
+ @WorkerThread
+ fun getFriends(userId: Int): List {
+ return AppGlobal.database.friends.getByUserId(userId)
+ }
+
+ fun appendUsers(users: Collection) {
+ for (user in users) {
+ append(user)
+ }
+ }
+
+ fun appendGroups(groups: Collection) {
+ for (group in groups) {
+ append(group)
+ }
+ }
+
+ fun append(value: VKGroup) {
+ groups.append(value.groupId, value)
+ }
+
+ fun append(value: VKUser) {
+ users.append(value.userId, value)
+ }
+
+ @WorkerThread
+ fun put(value: VKUser) {
+ append(value)
+
+ AppGlobal.database.users.insert(value)
+ }
+
+ @WorkerThread
+ fun putUsers(users: List) {
+ appendUsers(users)
+
+ AppGlobal.database.users.insert(users)
+ }
+
+ @WorkerThread
+ fun put(value: VKFriend) {
+ AppGlobal.database.friends.insert(value)
+ }
+
+ @WorkerThread
+ fun putFriends(friends: List) {
+ AppGlobal.database.friends.insert(friends)
+ }
+
+ @WorkerThread
+ fun put(value: VKMessage) {
+ AppGlobal.database.messages.insert(value)
+ }
+
+ @WorkerThread
+ fun putMessages(messages: List) {
+ AppGlobal.database.messages.insert(messages)
+ }
+
+ @WorkerThread
+ fun put(value: VKGroup) {
+ append(value)
+
+ AppGlobal.database.groups.insert(value)
+ }
+
+ @WorkerThread
+ fun putGroups(groups: List) {
+ appendGroups(groups)
+
+ AppGlobal.database.groups.insert(groups)
+ }
+
+ @WorkerThread
+ fun put(value: VKConversation) {
+ AppGlobal.database.conversations.insert(value)
+ }
+
+ @WorkerThread
+ fun putConversations(conversations: List) {
+ AppGlobal.database.conversations.insert(conversations)
+ }
+
+ @WorkerThread
+ fun deleteMessage(messageId: Int, safe: Boolean = true) {
+ if (safe) {
+ AppGlobal.database.messages.getById(messageId) ?: return
+ }
+
+ AppGlobal.database.messages.deleteById(messageId)
+ }
+
+ @WorkerThread
+ fun deleteConversation(conversationId: Int, safe: Boolean = true) {
+ if (safe) {
+ AppGlobal.database.conversations.getById(conversationId) ?: return
+ }
+
+ AppGlobal.database.conversations.deleteById(conversationId)
+ }
+
+ @WorkerThread
+ fun edit(message: VKMessage?, safe: Boolean = true) {
+ message ?: return
+
+ if (safe) {
+ AppGlobal.database.messages.getById(message.messageId) ?: return
+ }
+
+ AppGlobal.database.messages.update(message)
+ }
+
+ fun clear() {
+ users.clear()
+ groups.clear()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt
new file mode 100644
index 00000000..c46bfaba
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt
@@ -0,0 +1,38 @@
+package ru.melod1n.project.vkm.database.dao
+
+import androidx.room.*
+import ru.melod1n.project.vkm.api.model.VKConversation
+
+@Dao
+interface ConversationsDao {
+
+ @Query("SELECT * FROM conversations")
+ fun getAll(): List
+
+ @Query("SELECT * FROM conversations WHERE conversationId = :id")
+ fun getById(id: Int): VKConversation?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(item: VKConversation)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(items: List)
+
+ @Update
+ fun update(item: VKConversation)
+
+ @Update
+ fun update(items: List)
+
+ @Delete
+ fun delete(item: VKConversation)
+
+ @Delete
+ fun delete(items: List)
+
+ @Query("DELETE FROM conversations WHERE conversationId = :id")
+ fun deleteById(id: Int)
+
+ @Query("DELETE FROM conversations")
+ fun clear()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt
new file mode 100644
index 00000000..15cdbe1c
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt
@@ -0,0 +1,41 @@
+package ru.melod1n.project.vkm.database.dao
+
+import androidx.room.*
+import ru.melod1n.project.vkm.api.model.VKFriend
+
+@Dao
+interface FriendsDao {
+
+ @Query("SELECT * FROM friends")
+ fun getAll(): List
+
+ @Query("SELECT * FROM friends WHERE userId = :id")
+ fun getById(id: Int): VKFriend?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(item: VKFriend)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(items: List)
+
+ @Update
+ fun update(item: VKFriend)
+
+ @Update
+ fun update(items: List)
+
+ @Delete
+ fun delete(item: VKFriend)
+
+ @Delete
+ fun delete(items: List)
+
+ @Query("DELETE FROM friends WHERE userId = :id")
+ fun deleteById(id: Int)
+
+ @Query("DELETE FROM friends")
+ fun clear()
+
+ @Query("SELECT * FROM friends WHERE userId = :id")
+ fun getByUserId(id: Int): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt
new file mode 100644
index 00000000..539d1d1e
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt
@@ -0,0 +1,37 @@
+package ru.melod1n.project.vkm.database.dao
+
+import androidx.room.*
+import ru.melod1n.project.vkm.api.model.VKGroup
+
+@Dao
+interface GroupsDao {
+ @Query("SELECT * FROM groups")
+ fun getAll(): List
+
+ @Query("SELECT * FROM groups WHERE groupId = :id")
+ fun getById(id: Int): VKGroup?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(item: VKGroup)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(items: List)
+
+ @Update
+ fun update(item: VKGroup)
+
+ @Update
+ fun update(items: List)
+
+ @Delete
+ fun delete(item: VKGroup)
+
+ @Delete
+ fun delete(items: List)
+
+ @Query("DELETE FROM groups WHERE groupId = :id")
+ fun deleteById(id: Int)
+
+ @Query("DELETE FROM groups")
+ fun clear()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt
new file mode 100644
index 00000000..9e3e68b9
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt
@@ -0,0 +1,41 @@
+package ru.melod1n.project.vkm.database.dao
+
+import androidx.room.*
+import ru.melod1n.project.vkm.api.model.VKMessage
+
+@Dao
+interface MessagesDao {
+
+ @Query("SELECT * FROM messages")
+ fun getAll(): List
+
+ @Query("SELECT * FROM messages WHERE messageId = :id")
+ fun getById(id: Int): VKMessage?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(item: VKMessage)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(items: List)
+
+ @Update
+ fun update(item: VKMessage)
+
+ @Update
+ fun update(items: List)
+
+ @Delete
+ fun delete(item: VKMessage)
+
+ @Delete
+ fun delete(items: List)
+
+ @Query("DELETE FROM messages WHERE messageId = :id")
+ fun deleteById(id: Int)
+
+ @Query("DELETE FROM messages")
+ fun clear()
+
+ @Query("SELECT * FROM messages WHERE peerId = :peerId")
+ fun getByPeerId(peerId: Int): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt
new file mode 100644
index 00000000..15ad7537
--- /dev/null
+++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt
@@ -0,0 +1,38 @@
+package ru.melod1n.project.vkm.database.dao
+
+import androidx.room.*
+import ru.melod1n.project.vkm.api.model.VKUser
+
+@Dao
+interface UsersDao {
+
+ @Query("SELECT * FROM users")
+ fun getAll(): List
+
+ @Query("SELECT * FROM users WHERE userId = :id")
+ fun getById(id: Int): VKUser?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(item: VKUser)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(items: List)
+
+ @Update
+ fun update(item: VKUser)
+
+ @Update
+ fun update(items: List