Initial commit

This commit is contained in:
2021-02-18 13:55:03 +03:00
parent 12cb5eb34c
commit 88dddcb133
315 changed files with 16030 additions and 81 deletions
+1
View File
@@ -0,0 +1 @@
/build
+72
View File
@@ -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}"
}
+21
View File
@@ -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
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.melod1n.project.vkm">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".common.AppGlobal"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activity.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.LongPollService"
android:enabled="true" />
<activity android:name=".activity.MessagesActivity" />
<activity
android:name=".activity.StartActivity"
android:theme="@style/AppTheme.Start" />
<activity
android:name=".activity.LoginActivity"
android:label="@string/activity_login" />
<activity android:name=".activity.DropUserDataActivity" />
<activity
android:name=".activity.SettingsActivity"
android:label="@string/navigation_settings" />
<activity android:name=".activity.UpdateActivity" />
<receiver
android:name=".receiver.MinuteReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.ACTION_TIME_CHANGED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -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()
}
}
@@ -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()
}
}
}
@@ -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<VKUser> {
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()
}
}
}
@@ -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<VKModel>()
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
}
}
@@ -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()
}
}
}
@@ -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<AppCompatEditText>(R.id.customDataUserId)
val token = view.findViewById<AppCompatEditText>(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()
}
}
@@ -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<Any?> {
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()
}
}
}
@@ -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<VKMessage, MessagesRepository, MessagesView>(
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<VKConversation> {
override fun onResponse(response: VKConversation) {
conversation = response
createAdapter()
refreshConversation(response)
getCachedMessages(peerId, 0, DEFAULT_MESSAGES_COUNT,
object : MvpOnLoadListener<Any?> {
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<VKConversation> {
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<String> {
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<Any?>? = null
) {
repository.getCachedMessages(peerId, offset, count,
object : MvpOnLoadListener<ArrayList<VKMessage>> {
override fun onResponse(response: ArrayList<VKMessage>) {
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<ArrayList<VKMessage>> {
override fun onResponse(response: ArrayList<VKMessage>) {
fillAdapter(response, offset)
}
override fun onError(t: Throwable) {
}
})
}
private fun fillAdapter(
messages: ArrayList<VKMessage>,
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<VKModel> = 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<Int> {
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)
}
})
}
}
@@ -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<VKMessage>() {
fun loadMessages(
peerId: Int,
offset: Int,
count: Int,
listener: MvpOnLoadListener<ArrayList<VKMessage>>
) {
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<ArrayList<VKMessage>> {
override fun onResponse(response: ArrayList<VKMessage>) {
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<ArrayList<VKMessage>>
) {
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<VKConversation>) {
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<VKConversation>) {
TaskManager.loadConversation(
VKApiKeys.UPDATE_CONVERSATION,
peerId,
object : OnResponseListener<VKConversation> {
override fun onResponse(response: VKConversation) {
sendResponse(listener, response)
}
override fun onError(t: Throwable) {
sendError(listener, t)
}
})
}
fun getChatInfo(conversation: VKConversation, listener: MvpOnLoadListener<String>) {
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<VKUser> {
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<Int>
) {
TaskManager.execute {
VKApi.messages()
.send()
.peerId(peerId)
.message(message)
.randomId(randomId)
.executeArray(Int::class.java, object : OnResponseListener<ArrayList<Int>> {
override fun onResponse(response: ArrayList<Int>) {
val messageId = response[0]
sendResponse(listener, messageId)
}
override fun onError(t: Throwable) {
sendError(listener, t)
}
})
}
}
private fun cacheLoadedMessages(messages: ArrayList<VKMessage>) {
MemoryCache.putMessages(messages)
}
}
@@ -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)
}
@@ -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<VKConversation>
) : BaseAdapter<VKConversation, ConversationsAdapter.ConversationHolder>(
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<Int>)[0],
(info.data as Array<Int>)[1]
)
VKApiKeys.DELETE_MESSAGE -> deleteMessage(
(info.data as Array<Int>)[0],
(info.data as Array<Int>)[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<Int>)
VKApiKeys.UPDATE_GROUP -> updateGroups(info.data as ArrayList<Int>)
}
}
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<Any>
) {
currentPosition = position
initListeners(holder.itemView, position)
holder.bind(position, payloads)
}
fun notifyChanges(oldList: List<VKConversation>, newList: List<VKConversation> = 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<Any>) {
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<String> {
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<VKConversation>().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<Int>) {
for (groupId in groupIds) {
val index = searchConversationIndex(groupId)
if (index == -1) return
notifyItemChanged(index)
}
}
private fun updateUsers(userIds: ArrayList<Int>) {
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) }
}
}
}
@@ -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<VKMessage>,
var conversation: VKConversation
) : BaseAdapter<VKMessage, MessagesAdapter.Holder>(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<Int>)[0],
(info.data as Array<Int>)[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<Int>)[0],
(info.data as Array<Int>)[1]
)
VKApiKeys.UPDATE_MESSAGE -> updateMessage(info.data as Int)
VKApiKeys.UPDATE_USER -> updateUser(info.data as ArrayList<Int>)
VKApiKeys.UPDATE_GROUP -> updateGroup(info.data as ArrayList<Int>)
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<String> {
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<Int>) {
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<Int>) {
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()
}
}
@@ -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<SimpleMenuItem>) :
BaseAdapter<SimpleMenuItem, SimpleItemAdapter.ViewHolder>(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)
}
}
}
@@ -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<VKUser>) :
BaseAdapter<VKUser, UsersAdapter.ViewHolder>(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<VKUser>, newList: List<VKUser> = values) {
val callback = UsersCallback(oldList, newList)
val diff = DiffUtil.calculateDiff(callback, false)
diff.dispatchUpdatesTo(this)
}
}
@@ -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<VKConversation>,
private val newList: List<VKConversation>
) : 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)
}
}
@@ -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<VKUser>, private val newList: List<VKUser>) : 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)
}
}
@@ -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
}
@@ -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)
}
}
@@ -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 <T> execute(url: String, cls: Class<T>?): ArrayList<T>? {
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<T>?
}
}
Boolean::class.java -> {
val value = json.optInt("response") == 1
return arrayListOf(value) as ArrayList<T>?
}
Long::class.java -> {
val value = json.optLong("response")
return arrayListOf(value) as ArrayList<T>?
}
Int::class.java -> {
val value = json.optInt("response")
return arrayListOf(value) as ArrayList<T>?
}
}
val response = json.opt("response") ?: return null
val array = optItems(json) ?: return null
val models = ArrayList<T>(array.length())
when (cls) {
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<VKUser>()
for (j in 0 until it.length()) {
profiles.add(VKUser(it.optJSONObject(j)))
}
VKMessage.profiles = profiles
}
response.optJSONArray("groups")?.let {
val groups = arrayListOf<VKGroup>()
for (j in 0 until it.length()) {
groups.add(VKGroup(it.optJSONObject(j)))
}
VKMessage.groups = groups
}
response.optJSONArray("conversations")?.let {
val conversations = arrayListOf<VKConversation>()
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<VKUser>()
response.optJSONArray("profiles")?.let {
profiles.addAll(VKUser.parse(it))
}
val groups = ArrayList<VKGroup>()
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<T>
}
}
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<VKUser>()
for (j in 0 until it.length()) {
profiles.add(VKUser(it.optJSONObject(j)))
}
VKConversation.profiles = profiles
}
response.optJSONArray("groups")?.let {
val groups = arrayListOf<VKGroup>()
for (j in 0 until it.length()) {
groups.add(VKGroup(it.optJSONObject(j)))
}
VKConversation.groups = groups
}
models.add(conversation as T)
}
}
}
return models
}
fun <E> execute(url: String, cls: Class<E>, listener: OnResponseListener<E>?) {
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 <E> executeArray(url: String, cls: Class<E>, listener: OnResponseListener<ArrayList<E>>?) {
TaskManager.execute {
try {
val models = execute(url, cls)
listener?.let {
AppGlobal.handler.post(SuccessArrayCallback(listener, models as ArrayList<E>))
}
} 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<E>(
private val listener: OnResponseListener<E>?,
private val response: E
) : Runnable {
override fun run() {
listener?.onResponse(response)
}
}
class SuccessArrayCallback<E>(
private val listener: OnResponseListener<ArrayList<E>>?,
private val response: ArrayList<E>
) : Runnable {
override fun run() {
listener?.onResponse(response)
}
}
class ErrorCallback<E>(
private val listener: OnResponseListener<E>?,
private val exception: Exception
) : Runnable {
override fun run() {
listener?.onError(exception)
}
}
}
@@ -0,0 +1,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
}
@@ -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<String> {
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)
}
}
@@ -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"
}
}
@@ -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)
}
}
@@ -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<Int>): MessageMethodSetter {
put("message_ids", ArrayUtils.asString(ids))
return this
}
fun q(query: String): MessageMethodSetter {
put("q", query)
return this
}
fun startMessageId(id: Int): MessageMethodSetter {
put("start_message_id", id)
return this
}
fun peerId(value: Int): MessageMethodSetter {
put("peer_id", value)
return this
}
fun peerIds(vararg values: Int): MessageMethodSetter {
put("peer_ids", ArrayUtils.asString(values))
return this
}
fun reversed(value: Boolean): MessageMethodSetter {
put("rev", value)
return this
}
fun domain(value: String): MessageMethodSetter {
put("domain", value)
return this
}
fun chatId(value: Int): MessageMethodSetter {
put("chat_id", value)
return this
}
fun message(message: String): MessageMethodSetter {
put("message", message)
return this
}
fun randomId(value: Int): MessageMethodSetter {
put("random_id", value)
return this
}
fun lat(lat: Double): MessageMethodSetter {
put("lat", lat)
return this
}
fun longitude(value: Long): MessageMethodSetter {
put("LONG", value)
return this
}
fun attachment(attachments: Collection<String>): MessageMethodSetter {
put("attachment", ArrayUtils.asString(attachments))
return this
}
fun attachment(vararg attachments: String): MessageMethodSetter {
put("attachment", ArrayUtils.asString(*attachments))
return this
}
fun forwardMessages(ids: Collection<String>): 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<Int>): 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
}
}
@@ -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<String, String> = ArrayMap()
fun put(key: String, value: Any): MethodSetter {
params[key] = value.toString()
return this
}
fun put(key: String, value: String): MethodSetter {
params[key] = value
return this
}
fun put(key: String, value: Int): MethodSetter {
params[key] = value.toString()
return this
}
fun put(key: String, value: Long): MethodSetter {
params[key] = value.toString()
return this
}
fun put(key: String, value: Boolean): MethodSetter {
params[key] = if (value) "1" else "0"
return this
}
private fun getSignedUrl(): String {
if (!params.containsKey("access_token")) {
params["access_token"] = 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 <E> execute(cls: Class<E>): ArrayList<E>? {
return VKApi.execute(getSignedUrl(), cls)
}
fun <E> executeArray(cls: Class<E>, listener: OnResponseListener<ArrayList<E>>?) {
VKApi.executeArray(getSignedUrl(), cls, listener)
}
fun <E> execute(cls: Class<E>, listener: OnResponseListener<E>?) {
VKApi.execute(getSignedUrl(), cls, listener)
}
fun userId(value: Int): MethodSetter {
return put("user_id", value)
}
fun userIds(vararg ids: Int): MethodSetter {
return put("user_ids", ArrayUtils.asString(ids))
}
fun userIds(ids: ArrayList<Int>): 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<Int>): 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)
}
}
@@ -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
}
}
@@ -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<VKModel> {
val attachments = ArrayList<VKModel>(array.length())
for (i in 0 until array.length()) {
var attachment = array.optJSONObject(i) ?: continue
if (attachment.has("attachment")) {
attachment = attachment.optJSONObject("attachment") ?: continue
}
val type = 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
}
}
@@ -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")
}
@@ -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<Int>()
var linkOgg: String = o.optString("link_ogg")
var linkMp3: String = o.optString("link_mp3")
init {
o.optJSONArray("waveform")?.let {
val waveform = ArrayList<Int>()
for (i in 0 until it.length()) {
waveform.add(it.optInt(i))
}
this.waveform = waveform
}
}
}
@@ -0,0 +1,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")
}
@@ -0,0 +1,4 @@
package ru.melod1n.project.vkm.api.model
class VKComment { //https://vk.com/dev/objects/comment
}
@@ -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<VKUser>()
var groups = arrayListOf<VKGroup>()
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
}
}
@@ -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<VKPhotoSize>? = null
init {
o.optJSONArray("sizes")?.let {
val sizes = ArrayList<VKPhotoSize>()
for (i in 0 until it.length()) {
sizes.add(VKPhotoSize(it.optJSONObject(i)))
}
this.sizes = sizes
}
}
}
class Graffiti(o: JSONObject) {
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)
}
}
}
}
@@ -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
}
}
@@ -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")
}
@@ -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")
}
@@ -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<VKGroup> {
val groups = ArrayList<VKGroup>()
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")
}
@@ -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")
}
}
}
@@ -0,0 +1,12 @@
package ru.melod1n.project.vkm.api.model
import java.util.*
class VKLongPollHistory : VKModel() {
private val lpMessages: ArrayList<VKMessage>? = null
private val messages: ArrayList<VKMessage>? = null
private val profiles: ArrayList<VKUser>? = null
private val groups: ArrayList<VKGroup>? = null //TODO: использовать
}
@@ -0,0 +1,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")
}
@@ -0,0 +1,4 @@
package ru.melod1n.project.vkm.api.model
class VKMarketAlbum { //https://vk.com/dev/objects/market_album
}
@@ -0,0 +1,4 @@
package ru.melod1n.project.vkm.api.model
class VKMarketItem { //https://vk.com/dev/objects/market_item
}
@@ -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<VKUser>()
var groups = arrayListOf<VKGroup>()
var conversations = arrayListOf<VKConversation>()
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<String, Int>()
fun isOut(flags: Int): Boolean {
return OUTBOX and flags > 0
}
fun isDeleted(flags: Int): Boolean {
return DELETED and flags > 0
}
fun isUnread(flags: Int): Boolean {
return UNREAD and flags > 0
}
fun isSpam(flags: Int): Boolean {
return SPAM and flags > 0
}
fun isCanceledSpam(flags: Int): Boolean {
return CANCEL_SPAM and flags > 0
}
fun isImportant(flags: Int): Boolean {
return IMPORTANT and flags > 0
}
fun isDeletedForAll(flags: Int): Boolean {
return DELETE_FOR_ALL and flags > 0
}
init {
flags["unread"] = UNREAD
flags["outbox"] = OUTBOX
flags["replied"] = REPLIED
flags["important"] = IMPORTANT
flags["friends"] = FRIENDS
flags["spam"] = SPAM
flags["deleted"] = DELETED
flags["audio_listened"] = AUDIO_LISTENED
flags["chat"] = CHAT
flags["cancel_spam"] = CANCEL_SPAM
flags["hidden"] = HIDDEN
flags["delete_for_all"] = DELETE_FOR_ALL
flags["chat_in"] = CHAT_IN
flags["reply_msg"] = REPLY_MSG
}
}
@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<VKModel> = arrayListOf()
// @Ignore
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
@TypeConverters(ForwardedConverter::class)
var fwdMessages: ArrayList<VKMessage> = 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<VKMessage>(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<VKMessage>().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()
}
}
}
@@ -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")
}
}
@@ -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
}
}
@@ -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<VKPhotoSize>? = null
init {
o.optJSONArray("sizes")?.let {
val sizes = ArrayList<VKPhotoSize>()
for (i in 0 until it.length()) {
sizes.add(VKPhotoSize(it.optJSONObject(i)))
}
this.sizes = sizes
}
}
}
@@ -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")
}
@@ -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<VKModel>? = null
var fwdMessages: ArrayList<VKMessage>? = null
init {
o.optJSONArray("attachments")?.let {
attachments = VKAttachments.parse(it)
}
//TODO: parse forwarded
}
}
@@ -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<Answer>()
var isAnonymous = o.optBoolean("anonymous")
var isMultiple = o.optBoolean("multiple")
var answerIds = ArrayList<Int>()
var endDate = o.optInt("end_date")
var isClosed = o.optBoolean("closed")
var isBoard = o.optBoolean("is_board")
var isCanEdit = o.optBoolean("can_edit")
var isCanVote = false
var isCanReport = false
var isCanShare = false
var authorId = 0
var background = Color.WHITE
//TODO: private ArrayList friends
init {
o.optJSONArray("answers")?.let {
val answers = ArrayList<Answer>()
for (i in 0 until it.length()) {
answers.add(Answer(it.optJSONObject(i)))
}
this.answers = answers
}
//setAnswerIds();
// ...
}
class Answer(o: JSONObject) : Serializable {
var id = o.optInt("id", -1)
var text: String = o.optString("text")
var votes = o.optInt("votes")
var rate = o.optInt("rate")
}
}
@@ -0,0 +1,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<Image>? = null
init {
o.optJSONArray("images")?.let {
val images = ArrayList<Image>()
for (i in 0 until it.length()) {
images.add(Image(it.optJSONObject(i)))
}
this.images = images
}
}
class Image(o: JSONObject) : VKModel() {
var url: String = o.optString("url")
var width = o.optInt("width")
var height = o.optInt("height")
}
}
@@ -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<VKUser> {
val users = ArrayList<VKUser>()
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"
}
}
@@ -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")
}
@@ -0,0 +1,4 @@
package ru.melod1n.project.vkm.api.model
class VKWall { //https://vk.com/dev/objects/post
}
@@ -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<VKMessage>,
firstOnTop: Boolean
): ArrayList<VKMessage> {
values.sortWith { m1, m2 ->
val d1 = m1.date
val d2 = m2.date
if (firstOnTop) {
d2 - d1
} else {
d1 - d2
}
}
return values
}
fun sortConversationsByDate(
values: ArrayList<VKConversation>,
firstOnTop: Boolean
): ArrayList<VKConversation> {
values.sortWith { c1, c2 ->
val d1 = c1.lastMessage.date
val d2 = c2.lastMessage.date
return@sortWith if (firstOnTop) {
d2 - d1
} else {
d1 - d2
}
}
return values
}
fun 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<VKUser>? = 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<VKGroup>? = 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<VKModel>): 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<VKModel> = 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<VKModel> = 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<VKModel>): 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<VKMessage>): 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<String>
) {
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<VKConversation> {
val conversations = arrayListOf<VKConversation>()
for (i in 0 until array.length()) {
conversations.add(VKConversation(array.optJSONObject(i)))
}
return conversations
}
fun parseMessages(array: JSONArray): ArrayList<VKMessage> {
val messages = arrayListOf<VKMessage>()
for (i in 0 until array.length()) {
messages.add(VKMessage(array.optJSONObject(i)))
}
return messages
}
fun 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
}
}
@@ -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)
}
}
@@ -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<T, VH : BaseHolder>(
var context: Context,
var values: ArrayList<T> = arrayListOf()
) : RecyclerView.Adapter<VH>() {
companion object {
private const val P_ITEMS = "BaseAdapter.values"
}
private var cleanValues: ArrayList<T>? = 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<T>) {
values.addAll(items)
cleanValues?.addAll(items)
}
fun addAll(position: Int, items: List<T>) {
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<T>) {
values.clear()
values.addAll(arrayList)
}
fun updateValues(list: List<T>) = 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<T>
}
}
}
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]
}
}
@@ -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)
}
}
@@ -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)
}
}
}
}
@@ -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)
}
@@ -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)
}
}
@@ -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"
}
@@ -0,0 +1,3 @@
package ru.melod1n.project.vkm.base.mvp
class MvpException(var errorId: String) : Exception(errorId)
@@ -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<String, Any>()
fun put(key: String, value: Any): MvpFields {
fields[key] = value
return this
}
operator fun <T> 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<String, Any> {
return fields
}
}
@@ -0,0 +1,9 @@
package ru.melod1n.project.vkm.base.mvp
interface MvpOnLoadListener<T> {
fun onResponse(response: T)
fun onError(t: Throwable)
}
@@ -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<MainItem, Repository : MvpRepository<MainItem>, 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()
}
}
}
}
@@ -0,0 +1,48 @@
package ru.melod1n.project.vkm.base.mvp
@Suppress("UNCHECKED_CAST")
abstract class MvpRepository<T> {
// abstract fun loadValues(fields: MvpFields, listener: MvpOnLoadListener<T>?)
//
// abstract fun loadCachedValues(fields: MvpFields, listener: MvpOnLoadListener<T>?)
//
// protected fun sendError(listener: MvpOnLoadListener<T>?, errorId: String) {
// listener?.onErrorLoad(MvpException(errorId))
// }
//
protected fun <Item> sendError(
listener: MvpOnLoadListener<Item>,
t: Throwable
) {
// if (listener !is MvpOnLoadListener) return
MvpBase.post { listener.onError(t) }
}
protected fun <Item> sendResponseArray(
listener: MvpOnLoadListener<ArrayList<Item>>,
response: ArrayList<Item>
) {
listener.let { MvpBase.handler.post { listener.onResponse(response) } }
}
protected fun <Item> sendResponse(
listener: MvpOnLoadListener<Item>,
response: Item
) {
listener.let {
MvpBase.handler.post { listener.onResponse(response) }
}
}
// protected open fun cacheLoadedValues(values: ArrayList<T>) {}
//
// protected fun startNewThread(runnable: Runnable) {
// Thread(runnable).start()
// }
//
// protected fun post(runnable: Runnable) {
// MvpBase.handler.post(runnable)
// }
}
@@ -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()
}
@@ -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
}
}
}
@@ -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<Fragment>
) {
val transaction = fragmentManager.beginTransaction()
for (fragment in fragments) {
transaction.add(containerId, fragment, fragment.javaClass.simpleName)
}
transaction.commitNow()
}
fun showFragment(fragmentManager: FragmentManager, tag: String) {
showFragment(fragmentManager, tag, false)
}
fun showFragment(
fragmentManager: FragmentManager,
tag: String,
hideOthers: Boolean,
containerId: Int = R.id.fragmentContainer
) {
val fragments = fragmentManager.fragments
if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty")
var fragmentToShow: Fragment? = null
for (fragment in fragments) {
if (fragment.tag != null && fragment.tag == tag) {
fragmentToShow = fragment
break
}
}
val transaction = fragmentManager.beginTransaction()
if (fragmentToShow == null) {
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()
}
}
}
@@ -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<Int>()
private var groupsTimer: Timer? = null
private val usersTasksIds = arrayListOf<Int>()
private var usersTimer: Timer? = null
private val messagesTasksIds = arrayListOf<Int>()
private val messagesReadIds = arrayListOf<Int>()
private var messagesReadTimer: Timer? = null
private val conversationsTasksIds = arrayListOf<Int>()
private val listeners = arrayListOf<OnEventListener?>()
fun addOnEventListener(listener: OnEventListener?) {
listeners.add(listener)
}
fun removeOnEventListener(listener: OnEventListener?) {
listeners.remove(listener)
}
fun execute(runnable: Runnable?) {
LowThread(runnable).start()
}
private fun <T> addProcedure(
methodSetter: MethodSetter,
className: Class<T>,
pushInfo: EventInfo<*>?,
responseListener: OnResponseListener<T>?
) {
execute {
methodSetter.executeArray(className, object : OnResponseListener<ArrayList<T>> {
override fun onResponse(response: ArrayList<T>) {
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<VKUser>? = 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<String>()
usersTasksIds.forEach { usersIds.add(it.toString()) }
addProcedure(
setter,
VKUser::class.java,
EventInfo(eventKey, usersTasksIds),
object : OnResponseListener<VKUser> {
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<VKGroup>? = 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<String>()
groupsTasksIds.forEach { groupsIds.add(it.toString()) }
addProcedure(
setter,
VKGroup::class.java,
EventInfo(eventKey, groupsTasksIds),
object : OnResponseListener<VKGroup> {
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<VKMessage>? = 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<VKMessage> {
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<Any?>? = 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<String>()
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<Int> {
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<VKConversation>? = 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<VKConversation> {
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<*>)
}
}
@@ -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<OnHourChangeListener> = ArrayList()
private val onMinuteChangeListeners: ArrayList<OnMinuteChangeListener> = ArrayList()
private val onSecondChangeListeners: ArrayList<OnSecondChangeListener> = ArrayList()
private val onTimeChangeListeners: ArrayList<OnTimeChangeListener> = ArrayList()
fun init(context: Context) {
context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK"))
addOnMinuteChangeListener(minuteChangeListener)
}
private var minuteChangeListener = object : OnMinuteChangeListener {
override fun onMinuteChange(currentMinute: Int) {
TimeManager.currentMinute = currentMinute
}
}
fun destroy() {
removeOnMinuteChangeListener(minuteChangeListener)
}
fun broadcastMinute() {
for (onMinuteChangeListener in onMinuteChangeListeners) {
onMinuteChangeListener.onMinuteChange(0)
}
}
val isMorning = currentHour in 7..11
val isAfternoon = currentHour in 12..16
val isEvening = currentHour in 17..22
val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1
fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) {
TimeManager.onHourChangeListeners.add(onHourChangeListeners)
}
fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) {
onHourChangeListeners.remove(onHourChangeListener)
}
fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) {
onMinuteChangeListeners.add(onMinuteChangeListener)
}
fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) {
onMinuteChangeListeners.remove(onMinuteChangeListener)
}
fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) {
onSecondChangeListeners.add(onSecondChangeListener)
}
fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) {
onSecondChangeListeners.remove(onSecondChangeListener)
}
fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) {
onTimeChangeListeners.add(onTimeChangeListener)
}
fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
onTimeChangeListeners.remove(onTimeChangeListener)
}
interface OnHourChangeListener {
fun onHourChange(currentHour: Int)
}
interface OnMinuteChangeListener {
fun onMinuteChange(currentMinute: Int)
}
interface OnSecondChangeListener {
fun onSecondChange(currentSecond: Int)
}
interface OnTimeChangeListener {
fun onHourChange(currentHour: Int)
fun onMinuteChange(currentMinute: Int)
fun onSecondChange(currentSecond: Int)
}
}
@@ -0,0 +1,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<String, String>()
params["product"] = PRODUCT_NAME
params["branch"] = BRANCH
params["offset"] = OFFSET.toString()
params["code"] = AppGlobal.versionCode.toString()
if (BuildConfig.DEBUG) {
Log.d(TAG, "Request started")
}
HttpRequest[newLink, params].asString().let {
AppGlobal.post {
if (BuildConfig.DEBUG) {
Log.d(TAG, "response: $it")
}
val response: Any = if (it == "[]") JSONArray(it) else JSONObject(it)
val newUpdateInfo: NewUpdateInfo? =
if (response is JSONArray) null else NewUpdateInfo(response as JSONObject)
if (response is JSONArray || newUpdateInfo?.version?.isEmpty() == true || newUpdateInfo?.version == AppGlobal.versionName) {
onUpdateListener.onNoUpdates()
return@post
} else {
newUpdateInfo?.let { onUpdateListener.onNewUpdate(it) }
}
}
}
// HttpRequest[checkLink].asString().let {
// val response = JSONObject(it)
//
// val updateInfo = UpdateInfo(response)
//
// AppGlobal.handler.post {
// if (updateInfo.version.isEmpty() || updateInfo.version == AppGlobal.versionName) {
// onUpdateListener.onNoUpdates()
// return@post
// }
//
// if (AppGlobal.versionName != updateInfo.version) {
// onUpdateListener.onNewUpdate(updateInfo)
// }
// }
// }
}
}
}
@@ -0,0 +1,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()
}
}
@@ -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
}
@@ -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<VKUser>()
private val groups = SparseArray<VKGroup>()
@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<VKMessage> {
return AppGlobal.database.messages.getByPeerId(peerId)
}
@WorkerThread
fun getMessages(): List<VKMessage> {
return AppGlobal.database.messages.getAll()
}
@WorkerThread
fun getConversationById(id: Int): VKConversation? {
return AppGlobal.database.conversations.getById(id)
}
@WorkerThread
fun getConversations(): List<VKConversation> {
return AppGlobal.database.conversations.getAll()
}
@WorkerThread
fun getFriends(userId: Int): List<VKFriend> {
return AppGlobal.database.friends.getByUserId(userId)
}
fun appendUsers(users: Collection<VKUser>) {
for (user in users) {
append(user)
}
}
fun appendGroups(groups: Collection<VKGroup>) {
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<VKUser>) {
appendUsers(users)
AppGlobal.database.users.insert(users)
}
@WorkerThread
fun put(value: VKFriend) {
AppGlobal.database.friends.insert(value)
}
@WorkerThread
fun putFriends(friends: List<VKFriend>) {
AppGlobal.database.friends.insert(friends)
}
@WorkerThread
fun put(value: VKMessage) {
AppGlobal.database.messages.insert(value)
}
@WorkerThread
fun putMessages(messages: List<VKMessage>) {
AppGlobal.database.messages.insert(messages)
}
@WorkerThread
fun put(value: VKGroup) {
append(value)
AppGlobal.database.groups.insert(value)
}
@WorkerThread
fun putGroups(groups: List<VKGroup>) {
appendGroups(groups)
AppGlobal.database.groups.insert(groups)
}
@WorkerThread
fun put(value: VKConversation) {
AppGlobal.database.conversations.insert(value)
}
@WorkerThread
fun putConversations(conversations: List<VKConversation>) {
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()
}
}
@@ -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<VKConversation>
@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<VKConversation>)
@Update
fun update(item: VKConversation)
@Update
fun update(items: List<VKConversation>)
@Delete
fun delete(item: VKConversation)
@Delete
fun delete(items: List<VKConversation>)
@Query("DELETE FROM conversations WHERE conversationId = :id")
fun deleteById(id: Int)
@Query("DELETE FROM conversations")
fun clear()
}
@@ -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<VKFriend>
@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<VKFriend>)
@Update
fun update(item: VKFriend)
@Update
fun update(items: List<VKFriend>)
@Delete
fun delete(item: VKFriend)
@Delete
fun delete(items: List<VKFriend>)
@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<VKFriend>
}
@@ -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<VKGroup>
@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<VKGroup>)
@Update
fun update(item: VKGroup)
@Update
fun update(items: List<VKGroup>)
@Delete
fun delete(item: VKGroup)
@Delete
fun delete(items: List<VKGroup>)
@Query("DELETE FROM groups WHERE groupId = :id")
fun deleteById(id: Int)
@Query("DELETE FROM groups")
fun clear()
}
@@ -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<VKMessage>
@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<VKMessage>)
@Update
fun update(item: VKMessage)
@Update
fun update(items: List<VKMessage>)
@Delete
fun delete(item: VKMessage)
@Delete
fun delete(items: List<VKMessage>)
@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<VKMessage>
}
@@ -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<VKUser>
@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<VKUser>)
@Update
fun update(item: VKUser)
@Update
fun update(items: List<VKUser>)
@Delete
fun delete(item: VKUser)
@Delete
fun delete(items: List<VKUser>)
@Query("DELETE FROM users WHERE userId = :id")
fun deleteById(id: Int)
@Query("DELETE FROM users")
fun clear()
}
@@ -0,0 +1,24 @@
package ru.melod1n.project.vkm.database.dao.converters
import androidx.room.TypeConverter
import ru.melod1n.project.vkm.api.model.VKModel
import ru.melod1n.project.vkm.extensions.ArrayExtensions.isNullOrEmpty
import ru.melod1n.project.vkm.util.Utils
@Suppress("UNCHECKED_CAST")
class ArrayListToByteArrayConverter {
@TypeConverter
fun toForwarded(data: ByteArray?): ArrayList<VKModel> {
return if (data.isNullOrEmpty()) arrayListOf() else {
val deserializedData = Utils.deserialize(data)
if (deserializedData == null) arrayListOf() else deserializedData as ArrayList<VKModel>
}
}
@TypeConverter
fun fromForwarded(forwarded: List<VKModel>): ByteArray {
return Utils.serialize(forwarded) ?: return byteArrayOf()
}
}
@@ -0,0 +1,25 @@
package ru.melod1n.project.vkm.database.dao.converters
import androidx.room.TypeConverter
import ru.melod1n.project.vkm.api.model.VKMessage
import ru.melod1n.project.vkm.extensions.ArrayExtensions.isNullOrEmpty
import ru.melod1n.project.vkm.util.Utils
import java.util.*
@Suppress("UNCHECKED_CAST")
class ForwardedConverter {
@TypeConverter
fun toForwarded(data: ByteArray?): ArrayList<VKMessage> {
return if (data.isNullOrEmpty()) arrayListOf() else {
val deserializedData = Utils.deserialize(data)
if (deserializedData == null) arrayListOf() else deserializedData as ArrayList<VKMessage>
}
}
@TypeConverter
fun fromForwarded(forwarded: List<VKMessage>): ByteArray {
return Utils.serialize(forwarded) ?: return byteArrayOf()
}
}
@@ -0,0 +1,115 @@
package ru.melod1n.project.vkm.dialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ru.melod1n.project.vkm.R
import ru.melod1n.project.vkm.activity.SettingsActivity
import ru.melod1n.project.vkm.adapter.SimpleItemAdapter
import ru.melod1n.project.vkm.api.UserConfig
import ru.melod1n.project.vkm.base.BaseFullscreenDialog
import ru.melod1n.project.vkm.common.AppGlobal
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.DrawableExtensions.tint
import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById
import ru.melod1n.project.vkm.item.SimpleMenuItem
import ru.melod1n.project.vkm.listener.ItemClickListener
import ru.melod1n.project.vkm.util.ViewUtils
import ru.melod1n.project.vkm.widget.Toolbar
class AccountDialog : BaseFullscreenDialog(), ItemClickListener {
companion object {
const val TAG = "account_fullscreen_dialog"
}
private lateinit var adapter: SimpleItemAdapter
private lateinit var toolbar: Toolbar
private lateinit var recyclerView: RecyclerView
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var headerRoot: RelativeLayout
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_account, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initViews()
prepareToolbar()
prepareRecyclerView()
}
private fun initViews() {
toolbar = findViewById(R.id.toolbar)
recyclerView = findViewById(R.id.recyclerView)
refreshLayout = findViewById(R.id.refreshLayout)
headerRoot = findViewById(R.id.headerRoot)
}
private fun prepareToolbar() {
toolbar.navigationIcon = requireContext().drawable(R.drawable.ic_close)
.tint(requireContext().color(R.color.accent))
toolbar.setTitle(R.string.account_dialog_title)
toolbar.setTitleMode(Toolbar.TitleMode.SIMPLE)
toolbar.setNavigationClickListener { dismiss() }
MemoryCache.getUserById(UserConfig.userId)?.let {
AppGlobal.handler.post { ViewUtils.prepareNavigationHeader(headerRoot, it) }
}
}
private fun prepareRecyclerView() {
refreshLayout.isEnabled = false
recyclerView.layoutManager =
LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
recyclerView.setHasFixedSize(true)
createItemsAndAdapter()
}
private fun createItemsAndAdapter() {
val items = arrayListOf<SimpleMenuItem>()
SimpleMenuItem(
requireContext().drawable(R.drawable.ic_settings_outline)
.tint(requireContext().color(R.color.accent)),
requireContext().getString(R.string.navigation_settings)
) { openSettingsScreen() }.let { items.add(it) }
adapter = SimpleItemAdapter(requireContext(), items).also {
it.itemClickListener = this
}
recyclerView.adapter = adapter
}
private fun openSettingsScreen() {
startActivity(Intent(requireContext(), SettingsActivity::class.java))
}
override fun onItemClick(position: Int) {
val item = adapter.getItem(position)
item.clickListener?.let {
it.onClick(requireView().findViewById(android.R.id.content))
dismiss()
}
}
}
@@ -0,0 +1,115 @@
package ru.melod1n.project.vkm.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import ru.melod1n.project.vkm.R
import ru.melod1n.project.vkm.adapter.SimpleItemAdapter
import ru.melod1n.project.vkm.api.model.VKConversation
import ru.melod1n.project.vkm.api.model.VKUser
import ru.melod1n.project.vkm.database.MemoryCache
import ru.melod1n.project.vkm.item.SimpleMenuItem
open class ProfileDialog(
private val conversation: VKConversation,
private val chatTitle: String
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "profile_bottom_sheet_dialog"
}
private lateinit var title: TextView
private lateinit var subtitle: TextView
private lateinit var recyclerView: RecyclerView
private lateinit var root: LinearLayout
private var adapter: SimpleItemAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.AppTheme_ProfileDialog)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_profile_bottom, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
title = view.findViewById(R.id.profileTitle)
subtitle = view.findViewById(R.id.profileSubtitle)
recyclerView = view.findViewById(R.id.profileItemMenu)
root = view.findViewById(R.id.profileRoot)
val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
recyclerView.layoutManager = layoutManager
title.text = chatTitle
subtitle.text = getSubtitle()
val items = ArrayList<SimpleMenuItem>()
items.add(
SimpleMenuItem(
ContextCompat.getDrawable(
requireContext(),
R.drawable.ic_search
)!!, "Search"
)
)
createAdapter(items)
}
private fun createAdapter(items: ArrayList<SimpleMenuItem>) {
adapter = SimpleItemAdapter(requireContext(), items)
recyclerView.adapter = adapter
}
private fun getSubtitle(): String {
return when (conversation.type) {
VKConversation.TYPE_CHAT -> getString(
R.string.chat_members,
conversation.membersCount
)
VKConversation.TYPE_GROUP -> {
val group = MemoryCache.getGroupById(conversation.conversationId) ?: return ""
"@${group.screenName}"
}
VKConversation.TYPE_USER -> {
// val user = MemoryCache.getUserById(conversation.id) ?: return ""
//TODO: придумать чо делать
val user: VKUser = null ?: return ""
var str =
if (user.screenName.contains("id${user.userId}")) "" else "@${user.screenName}"
val online =
getString(if (user.isOnlineMobile) R.string.user_online_mobile else if (user.isOnline) R.string.user_online else R.string.user_offline)
str += if (str.isEmpty()) online else " · $online"
str
}
else -> ""
}
}
}
@@ -0,0 +1,5 @@
package ru.melod1n.project.vkm.event
import ru.melod1n.project.vkm.api.VKApiKeys
class EventInfo<T> constructor(var key: VKApiKeys, var data: T? = null)
@@ -0,0 +1,13 @@
package ru.melod1n.project.vkm.extensions
import android.content.Context
object ArrayExtensions {
fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty()
fun <E> List<E>.asArrayList(): ArrayList<E> {
return ArrayList(this)
}
}
@@ -0,0 +1,37 @@
package ru.melod1n.project.vkm.extensions
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.*
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
object ContextExtensions {
fun Context.drawable(@DrawableRes resId: Int): Drawable? {
return ContextCompat.getDrawable(this, resId)
}
@ColorInt
fun Context.color(@ColorRes resId: Int): Int {
return ContextCompat.getColor(this, resId)
}
fun Context.font(@FontRes resId: Int): Typeface? {
return ResourcesCompat.getFont(this, resId)
}
fun Context.string(@StringRes resId: Int): String {
return getString(resId)
}
fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View {
return LayoutInflater.from(this).inflate(resId, root, attachToRoot)
}
}
@@ -0,0 +1,16 @@
package ru.melod1n.project.vkm.extensions
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
object DrawableExtensions {
fun Drawable?.tint(@ColorInt color: Int): Drawable? {
this?.setTint(color)
return this
}
}
@@ -0,0 +1,11 @@
package ru.melod1n.project.vkm.extensions
import kotlin.math.roundToInt
object FloatExtensions {
fun Float.int(): Int {
return roundToInt()
}
}
@@ -0,0 +1,16 @@
package ru.melod1n.project.vkm.extensions
import android.view.View
import androidx.fragment.app.Fragment
object FragmentExtensions {
fun <T : View> Fragment.findViewById(resId: Int): T {
return requireView().findViewById(resId)
}
fun Fragment.runOnUiThread(runnable: Runnable) {
requireActivity().runOnUiThread(runnable)
}
}
@@ -0,0 +1,55 @@
package ru.melod1n.project.vkm.extensions
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import ru.melod1n.project.vkm.BuildConfig
object ImageViewExtensions {
fun ImageView.loadImage(
sourceUrl: String,
placeholder: Drawable? = null,
callback: Callback? = null
) {
if (sourceUrl.trim().isEmpty()) {
if (BuildConfig.DEBUG) {
Log.d("ImageView", "sourceUrl is empty")
}
return
}
val builder = Picasso.get().load(sourceUrl)
placeholder?.let { builder.placeholder(it) }
try {
builder.into(this, object : Callback {
override fun onSuccess() {
if (BuildConfig.DEBUG) {
Log.d("ImageView", "loaded photo from $sourceUrl")
}
callback?.onSuccess()
}
override fun onError(e: Exception?) {
if (BuildConfig.DEBUG) {
Log.d("ImageView", "error loading photo from $sourceUrl")
}
callback?.onError(e)
}
})
} catch (e: Exception) {
e.printStackTrace()
if (BuildConfig.DEBUG) {
Log.d("ImageView", "Error loading photo from $sourceUrl")
}
}
}
}
@@ -0,0 +1,11 @@
package ru.melod1n.project.vkm.extensions
import java.util.*
object StringExtensions {
fun String.lowerCase(): String {
return toLowerCase(Locale.getDefault())
}
}
@@ -0,0 +1,17 @@
package ru.melod1n.project.vkm.extensions
import android.widget.TextView
import com.google.android.material.textfield.TextInputLayout
object TextViewExtensions {
fun TextView.clear() {
text = ""
}
fun TextInputLayout.clear() {
editText?.setText("")
}
}
@@ -0,0 +1,186 @@
package ru.melod1n.project.vkm.fragment
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ru.melod1n.project.vkm.R
import ru.melod1n.project.vkm.activity.MessagesActivity
import ru.melod1n.project.vkm.api.UserConfig
import ru.melod1n.project.vkm.api.VKApiKeys
import ru.melod1n.project.vkm.base.BaseFragment
import ru.melod1n.project.vkm.common.AppGlobal
import ru.melod1n.project.vkm.common.TaskManager
import ru.melod1n.project.vkm.event.EventInfo
import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById
import ru.melod1n.project.vkm.fragment.ui.presenter.ConversationsPresenter
import ru.melod1n.project.vkm.fragment.ui.view.ConversationsView
import ru.melod1n.project.vkm.util.ViewUtils
import ru.melod1n.project.vkm.widget.Toolbar
@Suppress("UNCHECKED_CAST")
class FragmentConversations : BaseFragment(), ConversationsView {
private lateinit var presenter: ConversationsPresenter
private lateinit var toolbar: Toolbar
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var recyclerView: RecyclerView
private lateinit var progressBar: ProgressBar
private lateinit var noItemsView: LinearLayout
private lateinit var noInternetView: LinearLayout
private lateinit var errorView: LinearLayout
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_conversations, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initViews()
prepareToolbar()
prepareRecyclerView()
prepareRefreshLayout()
presenter = ConversationsPresenter(this)
presenter.setup(recyclerView, refreshLayout)
}
private fun initViews() {
toolbar = findViewById(R.id.toolbar)
recyclerView = findViewById(R.id.recyclerView)
refreshLayout = findViewById(R.id.refreshLayout)
progressBar = findViewById(R.id.progressBar)
noItemsView = findViewById(R.id.noItemsView)
noInternetView = findViewById(R.id.noInternetView)
errorView = findViewById(R.id.errorView)
}
private fun prepareToolbar() {
initToolbar(R.id.toolbar)
toolbar.title = getString(R.string.navigation_conversations)
setProfileAvatar()
TaskManager.addOnEventListener(object : TaskManager.OnEventListener {
override fun onNewEvent(info: EventInfo<*>) {
if (info.key == VKApiKeys.UPDATE_USER) {
val userIds = info.data as ArrayList<Int>
if (userIds.contains(UserConfig.userId)) {
setProfileAvatar()
}
}
}
})
}
private fun prepareRefreshLayout() {
refreshLayout.setColorSchemeResources(R.color.accent)
}
private fun prepareRecyclerView() {
val manager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
val decoration = DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
decoration.setDrawable(
ColorDrawable(
ContextCompat.getColor(
requireContext(),
R.color.divider
)
)
)
recyclerView.setHasFixedSize(true)
recyclerView.itemAnimator = null
recyclerView.addItemDecoration(decoration)
recyclerView.layoutManager = manager
}
private fun setProfileAvatar() {
TaskManager.execute {
AppGlobal.database.users.getById(UserConfig.userId)?.let {
if (it.photo100.isNotEmpty()) {
runOnUi {
toolbar.getAvatar().setImageURI(it.photo100)
}
}
}
}
}
override fun openChat(extras: Bundle) {
startActivity(Intent(requireContext(), MessagesActivity::class.java).putExtras(extras))
}
override fun showErrorSnackbar(t: Throwable) {
ViewUtils.showErrorSnackbar(requireView(), 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
}
}
@@ -0,0 +1,191 @@
package ru.melod1n.project.vkm.fragment
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ru.melod1n.project.vkm.R
import ru.melod1n.project.vkm.activity.MessagesActivity
import ru.melod1n.project.vkm.api.UserConfig
import ru.melod1n.project.vkm.api.VKApiKeys
import ru.melod1n.project.vkm.base.BaseFragment
import ru.melod1n.project.vkm.common.AppGlobal
import ru.melod1n.project.vkm.common.TaskManager
import ru.melod1n.project.vkm.event.EventInfo
import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById
import ru.melod1n.project.vkm.fragment.ui.presenter.FriendsPresenter
import ru.melod1n.project.vkm.fragment.ui.view.FriendsView
import ru.melod1n.project.vkm.util.ViewUtils
import ru.melod1n.project.vkm.widget.Toolbar
class FragmentFriends(private val userId: Int = 0) : BaseFragment(), FriendsView {
private lateinit var presenter: FriendsPresenter
private lateinit var toolbar: Toolbar
private lateinit var recyclerView: RecyclerView
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var progressBar: ProgressBar
private lateinit var noItemsView: LinearLayout
private lateinit var noInternetView: LinearLayout
private lateinit var errorView: LinearLayout
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_friends, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initViews()
prepareToolbar()
prepareRecyclerView()
prepareRefreshLayout()
presenter = FriendsPresenter(this)
presenter.setup(userId, recyclerView, refreshLayout)
}
private fun initViews() {
toolbar = findViewById(R.id.toolbar)
recyclerView = findViewById(R.id.recyclerView)
refreshLayout = findViewById(R.id.refreshLayout)
progressBar = findViewById(R.id.progressBar)
noItemsView = findViewById(R.id.noItemsView)
noInternetView = findViewById(R.id.noInternetView)
errorView = findViewById(R.id.errorView)
}
private fun prepareToolbar() {
initToolbar(R.id.toolbar)
toolbar.title = getString(R.string.navigation_friends)
setProfileAvatar()
toolbar.inflateMenu(R.menu.fragment_friends)
TaskManager.addOnEventListener(object : TaskManager.OnEventListener {
override fun onNewEvent(info: EventInfo<*>) {
if (info.key == VKApiKeys.UPDATE_USER) {
val userId = info.data as ArrayList<Int>
if (userId[0] == UserConfig.userId) {
setProfileAvatar()
}
}
}
})
}
private fun setProfileAvatar() {
TaskManager.execute {
AppGlobal.database.users.getById(UserConfig.userId)?.let {
if (it.photo100.isNotEmpty()) {
runOnUi {
toolbar.getAvatar().setImageURI(it.photo100)
}
}
}
}
}
override fun onDetach() {
presenter.destroy()
super.onDetach()
}
private fun prepareRefreshLayout() {
refreshLayout.setColorSchemeResources(R.color.accent)
}
private fun prepareRecyclerView() {
val manager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
val decoration = DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
decoration.setDrawable(
ColorDrawable(
ContextCompat.getColor(
requireContext(),
R.color.divider
)
)
)
recyclerView.addItemDecoration(decoration)
recyclerView.layoutManager = manager
}
override fun openChat(extras: Bundle) {
startActivity(Intent(requireContext(), MessagesActivity::class.java).putExtras(extras))
}
override fun showErrorSnackbar(t: Throwable) {
ViewUtils.showErrorSnackbar(requireView(), 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
}
}

Some files were not shown because too many files have changed in this diff Show More