Dark theme

This commit is contained in:
2021-02-20 23:21:25 +03:00
parent 88dddcb133
commit 06aa41cab1
179 changed files with 1011 additions and 1133 deletions
@@ -0,0 +1,23 @@
package com.meloda.fast.activity
import android.content.Intent
import android.os.Bundle
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKAuth
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.DrawableExtensions.tint
import com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.FragmentSwitcher
import com.meloda.fast.common.TaskManager
import com.meloda.fast.common.TimeManager
import com.meloda.fast.dialog.AccountDialog
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.DrawableExtensions.tint
import com.meloda.fast.fragment.FragmentConversations
import com.meloda.fast.fragment.FragmentFriends
import com.meloda.fast.fragment.FragmentSettings
import com.meloda.fast.fragment.LoginFragment
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.service.LongPollService
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ViewUtils
import com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.activity.ui.presenter.MessagesPresenter
import com.meloda.fast.activity.ui.view.MessagesView
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKGroup
import com.meloda.fast.api.model.VKModel
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.dialog.ProfileDialog
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.DrawableExtensions.tint
import com.meloda.fast.extensions.ImageViewExtensions.loadImage
import com.meloda.fast.fragment.FragmentSettings
import com.meloda.fast.util.KeyboardUtils
import com.meloda.fast.util.TextUtils
import com.meloda.fast.util.ViewUtils
import com.meloda.fast.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 com.meloda.fast.activity
import android.os.Bundle
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.FragmentSwitcher
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.DrawableExtensions.tint
import com.meloda.fast.fragment.FragmentSettings
import com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.common.UpdateManager
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.FloatExtensions.int
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.model.NewUpdateInfo
import com.meloda.fast.receiver.DownloadUpdateReceiver
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.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,256 @@
package com.meloda.fast.activity.ui.presenter
import androidx.recyclerview.widget.RecyclerView
import com.meloda.fast.R
import com.meloda.fast.activity.ui.repository.MessagesRepository
import com.meloda.fast.activity.ui.view.MessagesView
import com.meloda.fast.adapter.MessagesAdapter
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.model.VKModel
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.event.EventInfo
import com.meloda.fast.listener.ItemClickListener
import com.meloda.fast.listener.ItemLongClickListener
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpPresenter
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 com.meloda.fast.activity.ui.repository
import com.meloda.fast.R
import com.meloda.fast.api.VKApi
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKGroup
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.extensions.ArrayExtensions.asArrayList
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.util.ArrayUtils
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpRepository
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 com.meloda.fast.activity.ui.view
import com.meloda.fast.api.model.VKConversation
import com.meloda.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,687 @@
package com.meloda.fast.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.FrameLayout
import android.widget.ImageView
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 com.meloda.fast.R
import com.meloda.fast.adapter.diffutil.ConversationsCallback
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKGroup
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.base.BaseAdapter
import com.meloda.fast.base.BaseHolder
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.event.EventInfo
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.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: FrameLayout = 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))
}
} 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))
}
}
private fun setDialogType(conversation: VKConversation) {
val dDialogType = VKUtil.getDialogType(context, conversation)
type.setImageDrawable(dDialogType)
// type.isVisible = dDialogType != null
type.isVisible = false
}
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,589 @@
package com.meloda.fast.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 com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.activity.MessagesActivity
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.*
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.base.BaseAdapter
import com.meloda.fast.base.BaseHolder
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.event.EventInfo
import com.meloda.fast.extensions.FloatExtensions.int
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ImageUtils
import com.meloda.fast.widget.BoundedLinearLayout
import com.meloda.fast.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.setTextColor(
AndroidUtils.getThemeAttrColor(
context,
if (message.isOutbox()) R.attr.messageOutTextColor else R.attr.messageInTextColor
)
)
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 com.meloda.fast.adapter
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.meloda.fast.R
import com.meloda.fast.base.BaseAdapter
import com.meloda.fast.base.BaseHolder
import com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.adapter.diffutil.UsersCallback
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.base.BaseAdapter
import com.meloda.fast.base.BaseHolder
import com.meloda.fast.util.ImageUtils
import com.meloda.fast.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 com.meloda.fast.adapter.diffutil
import androidx.recyclerview.widget.DiffUtil
import com.meloda.fast.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,51 @@
package com.meloda.fast.adapter.diffutil
import androidx.recyclerview.widget.DiffUtil
import com.meloda.fast.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 com.meloda.fast.api
object ErrorCodes {
const val UNKNOWN_ERROR = 1
const val APP_DISABLED = 2
const val UNKNOWN_METHOD = 3
const val INVALID_SIGNATURE = 4
const val USER_AUTHORIZATION_FAILED = 5
const val TOO_MANY_REQUESTS = 6
const val NO_RIGHTS = 7
const val BAD_REQUEST = 8
const val TOO_MANY_SIMILAR_ACTIONS = 9
const val INTERNAL_SERVER_ERROR = 10
const val IN_TEST_MODE = 11
const val EXECUTE_CODE_COMPILE_ERROR = 12
const val EXECUTE_CODE_RUNTIME_ERROR = 13
const val CAPTCHA_NEEDED = 14
const val ACCESS_DENIED = 15
const val REQUIRES_REQUESTS_OVER_HTTPS = 16
const val VALIDATION_REQUIRED = 17
const val USER_BANNED_OR_DELETED = 18
const val ACTION_PROHIBITED = 20
const val ACTION_ALLOWED_ONLY_FOR_STANDALONE = 21
const val METHOD_OFF = 23
const val CONFIRMATION_REQUIRED = 24
const val PARAMETER_IS_NOT_SPECIFIED = 100
const val INCORRECT_APP_ID = 101
const val OUT_OF_LIMITS = 103
const val INCORRECT_USER_ID = 113
const val INCORRECT_TIMESTAMP = 150
const val ACCESS_TO_ALBUM_DENIED = 200
const val ACCESS_TO_AUDIO_DENIED = 201
const val ACCESS_TO_GROUP_DENIED = 203
const val ALBUM_IS_FULL = 300
const val ACTION_DENIED = 500
const val PERMISSION_DENIED = 600
const val CANNOT_SEND_MESSAGE_BLACK_LIST = 900
const val CANNOT_SEND_MESSAGE_GROUP = 901
const val INVALID_DOC_ID = 1150
const val INVALID_DOC_TITLE = 1152
const val ACCESS_TO_DOC_DENIED = 1153
}
@@ -0,0 +1,41 @@
package com.meloda.fast.api
import android.text.TextUtils
import com.meloda.fast.common.AppGlobal
object UserConfig {
private const val TOKEN = "token"
private const val USER_ID = "user_id"
const val API_ID = "6964679"
var 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,498 @@
package com.meloda.fast.api
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.annotation.WorkerThread
import com.meloda.fast.BuildConfig
import com.meloda.fast.activity.DropUserDataActivity
import com.meloda.fast.api.method.MessageMethodSetter
import com.meloda.fast.api.method.MethodSetter
import com.meloda.fast.api.method.UserMethodSetter
import com.meloda.fast.api.model.*
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.net.HttpRequest
import org.json.JSONArray
import org.json.JSONObject
import java.util.*
import kotlin.collections.ArrayList
@Suppress("UNCHECKED_CAST")
object VKApi {
private const val TAG = "VKM:VKApi"
const val BASE_URL = "https://api.vk.com/method/"
const val API_VERSION = "5.132"
val language: String = AppGlobal.locale.language
@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 com.meloda.fast.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 com.meloda.fast.api
import android.util.Log
import com.meloda.fast.BuildConfig
import com.meloda.fast.api.util.VKUtil
import java.net.URLEncoder
object VKAuth {
private const val TAG = "VKM.VKAuth"
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,15 @@
package com.meloda.fast.api
import java.io.IOException
class VKException(var url: String, override var message: String, var code: Int) :
IOException(message) {
var captchaSid: String? = null
var captchaImg: String? = null
var redirectUri: String? = null
override fun toString(): String {
return "code: $code, message: $message"
}
}
@@ -0,0 +1,146 @@
package com.meloda.fast.api
import android.util.Log
import androidx.annotation.WorkerThread
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.event.EventInfo
import org.json.JSONArray
@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 com.meloda.fast.api.method
import com.meloda.fast.util.ArrayUtils
class MessageMethodSetter(name: String) : MethodSetter(name) {
fun out(value: Boolean): MessageMethodSetter {
put("out", value)
return this
}
fun timeOffset(value: Int): MessageMethodSetter {
put("time_offset", value)
return this
}
fun filters(value: Int): MessageMethodSetter {
put("filters", value)
return this
}
fun previewLength(value: Int): MessageMethodSetter {
put("preview_length", value)
return this
}
fun lastMessageId(value: Int): MessageMethodSetter {
put("last_message_id", value)
return this
}
fun unread(value: Boolean): MessageMethodSetter {
put("unread", value)
return this
}
fun messageIds(vararg ids: Int): MessageMethodSetter {
put("message_ids", ArrayUtils.asString(ids))
return this
}
fun messageIds(ids: ArrayList<Int>): MessageMethodSetter {
put("message_ids", ArrayUtils.asString(ids))
return this
}
fun q(query: String): MessageMethodSetter {
put("q", query)
return this
}
fun startMessageId(id: Int): MessageMethodSetter {
put("start_message_id", id)
return this
}
fun peerId(value: Int): MessageMethodSetter {
put("peer_id", value)
return this
}
fun peerIds(vararg values: Int): MessageMethodSetter {
put("peer_ids", 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 com.meloda.fast.api.method
import android.util.ArrayMap
import android.util.Log
import com.meloda.fast.BuildConfig
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApi
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.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 com.meloda.fast.api.method
class UserMethodSetter(name: String) : MethodSetter(name) {
fun extended(extended: Boolean): UserMethodSetter {
put("extended", extended)
return this
}
fun type(type: String): UserMethodSetter {
put("type", type)
return this
}
fun comment(comment: String): UserMethodSetter {
put("comment", comment)
return this
}
fun latitude(latitude: Float): UserMethodSetter {
put("latitude", latitude)
return this
}
fun longitude(longitude: Float): UserMethodSetter {
put("longitude", longitude)
return this
}
fun accuracy(accuracy: Int): UserMethodSetter {
put("accuracy", accuracy)
return this
}
fun timeout(timeout: Int): UserMethodSetter {
put("timeout", timeout)
return this
}
fun radius(radius: Int): UserMethodSetter {
put("radius", radius)
return this
}
}
@@ -0,0 +1,59 @@
package com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.api.model
class VKComment { //https://vk.com/dev/objects/comment
}
@@ -0,0 +1,144 @@
package com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.api.model
class VKMarketAlbum { //https://vk.com/dev/objects/market_album
}
@@ -0,0 +1,4 @@
package com.meloda.fast.api.model
class VKMarketItem { //https://vk.com/dev/objects/market_item
}
@@ -0,0 +1,177 @@
package com.meloda.fast.api.model
import android.util.ArrayMap
import androidx.room.*
import com.meloda.fast.database.dao.converters.ArrayListToByteArrayConverter
import com.meloda.fast.database.dao.converters.ForwardedConverter
import org.json.JSONObject
@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)
}
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 com.meloda.fast.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 com.meloda.fast.api.model
import java.io.Serializable
abstract class VKModel : Serializable {
companion object {
const val serialVersionUID = 1L
}
}
@@ -0,0 +1,28 @@
package com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.api.model
class VKWall { //https://vk.com/dev/objects/post
}
@@ -0,0 +1,680 @@
package com.meloda.fast.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 com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.model.*
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.StringExtensions.lowerCase
import com.meloda.fast.listener.OnResponseListener
import com.meloda.fast.util.TextUtils
import org.json.JSONArray
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 com.meloda.fast.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 com.meloda.fast.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 com.meloda.fast.common.AppGlobal
import com.meloda.fast.extensions.ArrayExtensions.asArrayList
import com.meloda.fast.listener.ItemClickListener
import com.meloda.fast.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,23 @@
package com.meloda.fast.base
import androidx.annotation.IdRes
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import com.meloda.fast.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 com.meloda.fast.widget.Toolbar) it.initToolbar(
toolbar
)
}
}
protected fun runOnUi(runnable: Runnable) {
activity?.runOnUiThread(runnable)
}
}
@@ -0,0 +1,33 @@
package com.meloda.fast.base
import android.os.Bundle
import android.view.ViewGroup
import android.view.WindowManager
import androidx.fragment.app.DialogFragment
import com.meloda.fast.R
abstract class BaseFullscreenDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.AppTheme_FullScreenDialog)
}
override fun onStart() {
super.onStart()
dialog?.let { dialog ->
val width = ViewGroup.LayoutParams.MATCH_PARENT
val height = ViewGroup.LayoutParams.MATCH_PARENT
dialog.window?.let {
it.setLayout(width, height)
it.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
it.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
it.setWindowAnimations(R.style.AppTheme_Slide)
}
}
}
}
@@ -0,0 +1,8 @@
package com.meloda.fast.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,6 @@
package com.meloda.fast.base
abstract class FragmentStackActivity : BaseActivity() {
}
@@ -0,0 +1,161 @@
package com.meloda.fast.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.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.database.AppDatabase
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.fragment.FragmentSettings
import com.meloda.fast.util.AndroidUtils
import com.meloda.mvp.MvpBase
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 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()
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 {
return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
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 com.meloda.fast.common
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.meloda.fast.R
import com.meloda.fast.fragment.FragmentConversations
import com.meloda.fast.fragment.FragmentFriends
import com.meloda.fast.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 com.meloda.fast.common
import android.util.Log
import com.meloda.fast.BuildConfig
import com.meloda.fast.api.VKApi
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.api.method.MethodSetter
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKGroup
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.concurrent.LowThread
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.event.EventInfo
import com.meloda.fast.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 com.meloda.fast.common
import android.content.Context
import android.content.IntentFilter
import com.meloda.fast.receiver.MinuteReceiver
import java.util.*
object TimeManager {
var currentHour = 0
var currentMinute = 0
var currentSecond = 0
private val onHourChangeListeners: ArrayList<OnHourChangeListener> = ArrayList()
private val onMinuteChangeListeners: ArrayList<OnMinuteChangeListener> = ArrayList()
private val onSecondChangeListeners: ArrayList<OnSecondChangeListener> = ArrayList()
private val onTimeChangeListeners: ArrayList<OnTimeChangeListener> = ArrayList()
fun init(context: Context) {
context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK"))
addOnMinuteChangeListener(minuteChangeListener)
}
private var minuteChangeListener = object : OnMinuteChangeListener {
override fun onMinuteChange(currentMinute: Int) {
TimeManager.currentMinute = currentMinute
}
}
fun destroy() {
removeOnMinuteChangeListener(minuteChangeListener)
}
fun broadcastMinute() {
for (onMinuteChangeListener in onMinuteChangeListeners) {
onMinuteChangeListener.onMinuteChange(0)
}
}
val isMorning = currentHour in 7..11
val isAfternoon = currentHour in 12..16
val isEvening = currentHour in 17..22
val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1
fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) {
TimeManager.onHourChangeListeners.add(onHourChangeListeners)
}
fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) {
onHourChangeListeners.remove(onHourChangeListener)
}
fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) {
onMinuteChangeListeners.add(onMinuteChangeListener)
}
fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) {
onMinuteChangeListeners.remove(onMinuteChangeListener)
}
fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) {
onSecondChangeListeners.add(onSecondChangeListener)
}
fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) {
onSecondChangeListeners.remove(onSecondChangeListener)
}
fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) {
onTimeChangeListeners.add(onTimeChangeListener)
}
fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
onTimeChangeListeners.remove(onTimeChangeListener)
}
interface OnHourChangeListener {
fun onHourChange(currentHour: Int)
}
interface OnMinuteChangeListener {
fun onMinuteChange(currentMinute: Int)
}
interface OnSecondChangeListener {
fun onSecondChange(currentSecond: Int)
}
interface OnTimeChangeListener {
fun onHourChange(currentHour: Int)
fun onMinuteChange(currentMinute: Int)
fun onSecondChange(currentSecond: Int)
}
}
@@ -0,0 +1,80 @@
package com.meloda.fast.common
import android.util.Log
import androidx.collection.arrayMapOf
import com.meloda.fast.BuildConfig
import com.meloda.fast.model.NewUpdateInfo
import com.meloda.fast.net.HttpRequest
import org.json.JSONArray
import org.json.JSONObject
object UpdateManager {
interface OnUpdateListener {
fun onNewUpdate(updateInfo: NewUpdateInfo)
fun onNoUpdates()
}
private const val checkLink = "https://melodev.procsec.top/vkm/project_vkm_ota.json"
private const val PRODUCT_NAME = "project_vkm"
private const val BRANCH = "alpha"
private const val OFFSET = 0
private const val TAG = "UpdateManager"
fun checkUpdates(onUpdateListener: OnUpdateListener) {
TaskManager.execute {
val newLink = "https://temply.procsec.top/prop/deploy/api/method/getOTA"
val params = arrayMapOf<String, String>()
params["product"] = PRODUCT_NAME
params["branch"] = BRANCH
params["offset"] = OFFSET.toString()
params["code"] = AppGlobal.versionCode.toString()
if (BuildConfig.DEBUG) {
Log.d(TAG, "Request started")
}
HttpRequest[newLink, params].asString().let {
AppGlobal.post {
if (BuildConfig.DEBUG) {
Log.d(TAG, "response: $it")
}
val response: Any = if (it == "[]") JSONArray(it) else JSONObject(it)
val newUpdateInfo: NewUpdateInfo? =
if (response is JSONArray) null else NewUpdateInfo(response as JSONObject)
if (response is JSONArray || newUpdateInfo?.version?.isEmpty() == true || newUpdateInfo?.version == AppGlobal.versionName) {
onUpdateListener.onNoUpdates()
return@post
} else {
newUpdateInfo?.let { onUpdateListener.onNewUpdate(it) }
}
}
}
// HttpRequest[checkLink].asString().let {
// val response = JSONObject(it)
//
// val updateInfo = UpdateInfo(response)
//
// AppGlobal.handler.post {
// if (updateInfo.version.isEmpty() || updateInfo.version == AppGlobal.versionName) {
// onUpdateListener.onNoUpdates()
// return@post
// }
//
// if (AppGlobal.versionName != updateInfo.version) {
// onUpdateListener.onNewUpdate(updateInfo)
// }
// }
// }
}
}
}
@@ -0,0 +1,12 @@
package com.meloda.fast.concurrent
import android.os.Process
class LowThread(runnable: Runnable?) : Thread(runnable) {
override fun run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
super.run()
}
}
@@ -0,0 +1,20 @@
package com.meloda.fast.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.meloda.fast.api.model.*
import com.meloda.fast.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 com.meloda.fast.database
import android.util.SparseArray
import androidx.annotation.WorkerThread
import com.meloda.fast.api.model.*
import com.meloda.fast.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 com.meloda.fast.database.dao
import androidx.room.*
import com.meloda.fast.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 com.meloda.fast.database.dao
import androidx.room.*
import com.meloda.fast.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 com.meloda.fast.database.dao
import androidx.room.*
import com.meloda.fast.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 com.meloda.fast.database.dao
import androidx.room.*
import com.meloda.fast.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 com.meloda.fast.database.dao
import androidx.room.*
import com.meloda.fast.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 com.meloda.fast.database.dao.converters
import androidx.room.TypeConverter
import com.meloda.fast.api.model.VKModel
import com.meloda.fast.extensions.ArrayExtensions.isNullOrEmpty
import com.meloda.fast.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 com.meloda.fast.database.dao.converters
import androidx.room.TypeConverter
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.extensions.ArrayExtensions.isNullOrEmpty
import com.meloda.fast.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,117 @@
package com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.activity.SettingsActivity
import com.meloda.fast.adapter.SimpleItemAdapter
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.BaseFullscreenDialog
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.extensions.DrawableExtensions.tint
import com.meloda.fast.extensions.FragmentExtensions.findViewById
import com.meloda.fast.item.SimpleMenuItem
import com.meloda.fast.listener.ItemClickListener
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ColorUtils
import com.meloda.fast.util.ViewUtils
import com.meloda.fast.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(ColorUtils.getColorAccent(requireContext()))
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(ColorUtils.getColorAccent(requireContext())),
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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.adapter.SimpleItemAdapter
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.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 com.meloda.fast.event
import com.meloda.fast.api.VKApiKeys
class EventInfo<T> constructor(var key: VKApiKeys, var data: T? = null)
@@ -0,0 +1,11 @@
package com.meloda.fast.extensions
object ArrayExtensions {
fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty()
fun <E> List<E>.asArrayList(): ArrayList<E> {
return ArrayList(this)
}
}
@@ -0,0 +1,37 @@
package com.meloda.fast.extensions
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.*
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
object ContextExtensions {
fun Context.drawable(@DrawableRes resId: Int): Drawable? {
return ContextCompat.getDrawable(this, resId)
}
@ColorInt
fun Context.color(@ColorRes resId: Int): Int {
return ContextCompat.getColor(this, resId)
}
fun Context.font(@FontRes resId: Int): Typeface? {
return ResourcesCompat.getFont(this, resId)
}
fun Context.string(@StringRes resId: Int): String {
return getString(resId)
}
fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View {
return LayoutInflater.from(this).inflate(resId, root, attachToRoot)
}
}
@@ -0,0 +1,13 @@
package com.meloda.fast.extensions
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
object DrawableExtensions {
fun Drawable?.tint(@ColorInt color: Int): Drawable? {
this?.setTint(color)
return this
}
}
@@ -0,0 +1,11 @@
package com.meloda.fast.extensions
import kotlin.math.roundToInt
object FloatExtensions {
fun Float.int(): Int {
return roundToInt()
}
}
@@ -0,0 +1,16 @@
package com.meloda.fast.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) {
activity?.runOnUiThread(runnable)
}
}
@@ -0,0 +1,55 @@
package com.meloda.fast.extensions
import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.ImageView
import com.meloda.fast.BuildConfig
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
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 com.meloda.fast.extensions
import java.util.*
object StringExtensions {
fun String.lowerCase(): String {
return toLowerCase(Locale.getDefault())
}
}
@@ -0,0 +1,17 @@
package com.meloda.fast.extensions
import android.widget.TextView
import com.google.android.material.textfield.TextInputLayout
object TextViewExtensions {
fun TextView.clear() {
text = ""
}
fun TextInputLayout.clear() {
editText?.setText("")
}
}
@@ -0,0 +1,181 @@
package com.meloda.fast.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.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.meloda.fast.R
import com.meloda.fast.activity.MessagesActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.event.EventInfo
import com.meloda.fast.extensions.FragmentExtensions.findViewById
import com.meloda.fast.extensions.FragmentExtensions.runOnUiThread
import com.meloda.fast.fragment.ui.presenter.ConversationsPresenter
import com.meloda.fast.fragment.ui.view.ConversationsView
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ViewUtils
import com.meloda.fast.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(AndroidUtils.getThemeAttrColor(requireContext(), R.attr.dividerHorizontal))
)
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()) {
runOnUiThread {
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 com.meloda.fast.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 com.meloda.fast.R
import com.meloda.fast.activity.MessagesActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKApiKeys
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.event.EventInfo
import com.meloda.fast.extensions.FragmentExtensions.findViewById
import com.meloda.fast.fragment.ui.presenter.FriendsPresenter
import com.meloda.fast.fragment.ui.view.FriendsView
import com.meloda.fast.util.ViewUtils
import com.meloda.fast.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
}
}
@@ -0,0 +1,21 @@
package com.meloda.fast.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.meloda.fast.R
import com.meloda.fast.base.BaseFragment
class FragmentImportant : BaseFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_important, container, false)
}
}
@@ -0,0 +1,8 @@
package com.meloda.fast.fragment
import com.meloda.fast.base.BaseFragment
class FragmentSearch : BaseFragment() {
inner class SearchConversations : BaseFragment()
inner class SearchMessages : BaseFragment()
}
@@ -0,0 +1,218 @@
package com.meloda.fast.fragment
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import com.meloda.fast.R
import com.meloda.fast.activity.DropUserDataActivity
import com.meloda.fast.activity.UpdateActivity
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.TaskManager
import com.meloda.fast.extensions.ContextExtensions.color
import com.meloda.fast.extensions.ContextExtensions.drawable
import com.meloda.fast.util.AndroidUtils
class FragmentSettings : PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener {
companion object {
const val CATEGORY_GENERAL = "general"
const val KEY_HIDE_KEYBOARD_ON_SCROLL_UP = "hide_keyboard_on_scroll_up"
const val CATEGORY_APPEARANCE = "appearance"
const val KEY_EXTENDED_CONVERSATIONS = "appearance_extended_conversations"
const val KEY_THEME = "appearance_theme"
const val CATEGORY_ABOUT = "about"
const val KEY_APP_VERSION = "app_version"
const val CATEGORY_ACCOUNT = "account"
const val KEY_ACCOUNT_LOGOUT = "account_logout"
const val CATEGORY_DEBUG = "debug"
const val KEY_CLEAR_USERS_GROUPS_CACHE = "clear_users_groups_cache"
}
private var currentPreferenceLayout = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.fragment_settings, rootKey)
currentPreferenceLayout = R.xml.fragment_settings
init()
}
private fun init() {
setTitle()
setNavigationIcon()
setPreferencesFromResource(currentPreferenceLayout, null)
val general = findPreference<Preference>(CATEGORY_GENERAL)
general?.onPreferenceClickListener = rootLayoutClickListener
val account = findPreference<Preference>(CATEGORY_ACCOUNT)
account?.onPreferenceClickListener = rootLayoutClickListener
val logout = findPreference<Preference>(KEY_ACCOUNT_LOGOUT)
logout?.onPreferenceClickListener = this
val about = findPreference<Preference>(CATEGORY_ABOUT)
about?.onPreferenceClickListener = rootLayoutClickListener
val appVersion = findPreference<Preference>(KEY_APP_VERSION)
appVersion?.onPreferenceClickListener = this
val appearance = findPreference<Preference>(CATEGORY_APPEARANCE)
appearance?.onPreferenceClickListener = rootLayoutClickListener
val extendedConversations = findPreference<Preference>(KEY_EXTENDED_CONVERSATIONS)
extendedConversations?.onPreferenceChangeListener = this
val theme = findPreference<Preference>(KEY_THEME)
theme?.onPreferenceChangeListener = this
val debug = findPreference<Preference>(CATEGORY_DEBUG)
debug?.onPreferenceClickListener = rootLayoutClickListener
updateDebugCategoryVisibility()
val clearUsersGroupsCache = findPreference<Preference>(KEY_CLEAR_USERS_GROUPS_CACHE)
clearUsersGroupsCache?.onPreferenceClickListener = this
applyTintInPreferenceScreen(preferenceScreen)
}
override fun onResume() {
super.onResume()
updateDebugCategoryVisibility()
}
private fun updateDebugCategoryVisibility() {
findPreference<Preference>(CATEGORY_DEBUG)?.isVisible =
AndroidUtils.isDeveloperSettingsEnabled(requireContext())
}
private val rootLayoutClickListener =
Preference.OnPreferenceClickListener { changeRootLayout(it) }
private fun setNavigationIcon() {
val drawable =
if (currentPreferenceLayout == R.xml.fragment_settings) null
else requireContext().drawable(R.drawable.ic_arrow_back)
drawable?.setTint(requireContext().color(R.color.accent))
}
private fun setTitle() {
var title = R.string.navigation_settings
when (currentPreferenceLayout) {
R.xml.fragment_settings_general -> title = R.string.prefs_general
R.xml.fragment_settings_appearance -> title = R.string.prefs_appearance
R.xml.fragment_settings_about -> title = R.string.prefs_about
R.xml.fragment_settings_account -> title = R.string.prefs_account
}
requireActivity().setTitle(title)
}
private fun changeRootLayout(preference: Preference): Boolean {
currentPreferenceLayout = when (preference.key) {
CATEGORY_GENERAL -> R.xml.fragment_settings_general
CATEGORY_ABOUT -> R.xml.fragment_settings_about
CATEGORY_ACCOUNT -> R.xml.fragment_settings_account
CATEGORY_APPEARANCE -> R.xml.fragment_settings_appearance
CATEGORY_DEBUG -> R.xml.fragment_settings_debug
else -> R.xml.fragment_settings
}
init()
return true
}
private fun applyTintInPreferenceScreen(rootScreen: PreferenceScreen) {
if (rootScreen.preferenceCount > 0) {
for (i in 0 until rootScreen.preferenceCount) {
val preference = rootScreen.getPreference(i)
tintPreference(preference)
}
}
}
private fun tintPreference(preference: Preference) {
if (preference.icon != null && context != null) {
preference.icon.setTint(requireContext().color(R.color.accent))
}
}
override fun onPreferenceClick(preference: Preference): Boolean {
when (preference.key) {
KEY_ACCOUNT_LOGOUT -> {
logout()
return true
}
KEY_APP_VERSION -> {
openUpdateScreen()
return true
}
KEY_CLEAR_USERS_GROUPS_CACHE -> {
showClearCacheConfirmation()
}
}
return false
}
private fun showClearCacheConfirmation() {
val builder = AlertDialog.Builder(requireContext())
builder.setMessage("Clear cache?")
builder.setPositiveButton("Yes") { _, _ ->
TaskManager.execute {
AppGlobal.database.users.clear()
AppGlobal.database.groups.clear()
}
}
builder.setNegativeButton("No", null)
builder.show()
}
private fun openUpdateScreen() {
startActivity(Intent(requireContext(), UpdateActivity::class.java))
}
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
when (preference.key) {
KEY_EXTENDED_CONVERSATIONS -> {
return true
}
KEY_THEME -> {
AppGlobal.instance.applyNightMode(newValue as String)
(requireActivity() as BaseActivity).apply {
// applyNightMode()
finish()
startActivity(intent)
// recreate()
}
return true
}
}
return false
}
fun onBackPressed() = if (currentPreferenceLayout == R.xml.fragment_settings) {
true
} else {
currentPreferenceLayout = R.xml.fragment_settings
init()
false
}
private fun logout() {
startActivity(Intent(requireContext(), DropUserDataActivity::class.java))
}
}
@@ -0,0 +1,157 @@
package com.meloda.fast.fragment
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import com.google.android.material.button.MaterialButton
import com.meloda.fast.R
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.fragment.ui.presenter.LoginPresenter
import com.meloda.fast.fragment.ui.view.LoginView
import com.meloda.fast.util.KeyboardUtils
class LoginFragment : BaseFragment(), LoginView {
private lateinit var presenter: LoginPresenter
private lateinit var email: EditText
private lateinit var password: EditText
private lateinit var authorize: MaterialButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter = LoginPresenter(this)
presenter.onCreate(requireContext(), this, savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
presenter.onCreateView(savedInstanceState)
return inflater.inflate(R.layout.fragment_login, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onViewCreated(savedInstanceState)
}
override fun initViews() {
email = requireView().findViewById(R.id.loginEmailEditText)
password = requireView().findViewById(R.id.loginPasswordEditText)
authorize = requireView().findViewById(R.id.loginAuthorize)
}
override fun prepareViews() {
prepareEmailEditText()
preparePasswordEditText()
prepareAuthorizeButton()
}
private fun prepareEmailEditText() {
email.addTextChangedListener(onTextChangedListener)
}
private fun preparePasswordEditText() {
password.addTextChangedListener(onTextChangedListener)
password.setOnEditorActionListener { _, _, event ->
if (event == null) return@setOnEditorActionListener false
return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_DONE ||
(event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER))
) {
KeyboardUtils.hideKeyboardFrom(password)
authorize.performClick()
true
} else false
}
}
private fun prepareAuthorizeButton() {
authorize.isEnabled = false
authorize.setOnClickListener {
val emailString = email.text.toString().trim()
val passwordString = password.text.toString().trim()
presenter.login(emailString, passwordString)
}
}
override fun showErrorSnackbar(t: Throwable) {
TODO("Not yet implemented")
}
override fun prepareNoItemsView() {
TODO("Not yet implemented")
}
override fun showNoItemsView() {
TODO("Not yet implemented")
}
override fun hideNoItemsView() {
TODO("Not yet implemented")
}
override fun prepareNoInternetView() {
TODO("Not yet implemented")
}
override fun showNoInternetView() {
TODO("Not yet implemented")
}
override fun hideNoInternetView() {
TODO("Not yet implemented")
}
override fun prepareErrorView() {
TODO("Not yet implemented")
}
override fun showErrorView() {
TODO("Not yet implemented")
}
override fun hideErrorView() {
TODO("Not yet implemented")
}
override fun showProgressBar() {
TODO("Not yet implemented")
}
override fun hideProgressBar() {
TODO("Not yet implemented")
}
override fun showRefreshLayout() {
TODO("Not yet implemented")
}
override fun hideRefreshLayout() {
TODO("Not yet implemented")
}
private val onTextChangedListener = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
authorize.isEnabled =
email.text.toString().trim().isNotEmpty() &&
password.text.toString().trim().isNotEmpty()
}
override fun afterTextChanged(s: Editable?) {
}
}
}
@@ -0,0 +1,88 @@
package com.meloda.fast.fragment
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import com.meloda.fast.api.VKAuth
import com.meloda.fast.base.BaseFragment
class ValidationFragment : BaseFragment() {
private var url: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null && requireArguments().isEmpty.not()) {
url = requireArguments().getString("url") ?: ""
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val webView = WebView(requireContext())
webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
parseUrl(url ?: "")
}
}
webView.settings.domStorageEnabled = true
webView.clearCache(true)
webView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val manager = CookieManager.getInstance()
manager.removeAllCookies(null)
manager.flush()
manager.setAcceptCookie(true)
return webView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(requireView() as WebView).loadUrl(url)
}
private fun parseUrl(url: String) {
Log.d("WebView url", url)
try {
if (url.startsWith("https://oauth.vk.com/blank.html#success=1")) {
Log.d("Success WebView", "")
if (!url.contains("error=")) {
val auth = VKAuth.parseRedirectUrl(url)
val token = auth[0]
val userId = auth[1].toInt()
parentFragmentManager.setFragmentResult(
"validation",
bundleOf(
Pair("token", token),
Pair("userId", userId)
)
)
parentFragmentManager.popBackStack()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -0,0 +1,255 @@
package com.meloda.fast.fragment.ui.presenter
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.meloda.fast.BuildConfig
import com.meloda.fast.activity.MessagesActivity
import com.meloda.fast.adapter.ConversationsAdapter
import com.meloda.fast.adapter.diffutil.ConversationsCallback
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.common.TaskManager
import com.meloda.fast.common.TimeManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.fragment.ui.repository.ConversationsRepository
import com.meloda.fast.fragment.ui.view.ConversationsView
import com.meloda.fast.listener.ItemClickListener
import com.meloda.fast.listener.ItemLongClickListener
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ArrayUtils
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpPresenter
import java.util.*
class ConversationsPresenter(viewState: ConversationsView) :
MvpPresenter<VKConversation, ConversationsRepository, ConversationsView>(
viewState,
ConversationsRepository::class.java.name
),
ItemClickListener,
ItemLongClickListener,
TimeManager.OnMinuteChangeListener {
companion object {
const val DEFAULT_CONVERSATIONS_COUNT = 30
}
private var conversationsCount: Int = 0
private lateinit var adapter: ConversationsAdapter
private lateinit var recyclerView: RecyclerView
private lateinit var layoutManager: LinearLayoutManager
fun setup(recyclerView: RecyclerView, refreshLayout: SwipeRefreshLayout) {
this.recyclerView = recyclerView
this.context = recyclerView.context
this.layoutManager = recyclerView.layoutManager as LinearLayoutManager
prepareViews()
// setRecyclerViewScrollListener(recyclerView)
setRefreshLayoutListener(refreshLayout)
createAdapter()
TimeManager.addOnMinuteChangeListener(this)
loadConversations(0, DEFAULT_CONVERSATIONS_COUNT)
// getCachedConversations(0, DEFAULT_CONVERSATIONS_COUNT, object : MvpOnLoadListener<Any?> {
// override fun onResponse(response: Any?) {
// setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
// loadConversations(0, DEFAULT_CONVERSATIONS_COUNT)
// }
//
// override fun onError(t: Throwable) {
// setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
// loadConversations(0, DEFAULT_CONVERSATIONS_COUNT)
// }
// })
}
private fun setRecyclerViewScrollListener(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
if (adapter.isLastItem() && !adapter.isLoading && adapter.itemCount < conversationsCount) {
adapter.isLoading = true
val position = adapter.itemCount - 1
// adapter.itemCount - 1 - (layoutManager.findLastCompletelyVisibleItemPosition() - layoutManager.findFirstCompletelyVisibleItemPosition())
setState(ListState.FILLED_LOADING)
if (AndroidUtils.hasConnection()) {
loadConversations(adapter.itemCount, DEFAULT_CONVERSATIONS_COUNT,
object : MvpOnLoadListener<Any?> {
override fun onResponse(response: Any?) {
recyclerView.scrollToPosition(position)
adapter.isLoading = false
}
override fun onError(t: Throwable) {
viewState.showErrorSnackbar(t)
}
})
} else {
getCachedConversations(adapter.itemCount, DEFAULT_CONVERSATIONS_COUNT,
object : MvpOnLoadListener<Any?> {
override fun onResponse(response: Any?) {
recyclerView.scrollToPosition(position)
adapter.isLoading = false
}
override fun onError(t: Throwable) {
viewState.showErrorSnackbar(t)
}
})
}
if (BuildConfig.DEBUG)
Log.d("RecyclerView", "Bottom reached")
}
}
}
})
}
private fun setRefreshLayoutListener(refreshLayout: SwipeRefreshLayout) {
refreshLayout.setOnRefreshListener { loadConversations() }
}
private fun getCachedConversations(
offset: Int = 0,
count: Int = DEFAULT_CONVERSATIONS_COUNT,
listener: MvpOnLoadListener<Any?>? = null
) {
setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
repository.getCachedConversations(offset, count,
object : MvpOnLoadListener<ArrayList<VKConversation>> {
override fun onResponse(response: ArrayList<VKConversation>) {
conversationsCount = response.size
val conversations = ArrayUtils.cut(response, offset, count)
fillAdapter(conversations, offset)
setState(if (adapter.isEmpty()) ListState.EMPTY else ListState.FILLED)
listener?.onResponse(null)
}
override fun onError(t: Throwable) {
setState(if (adapter.isEmpty()) ListState.EMPTY_ERROR else ListState.FILLED)
listener?.onError(t)
}
})
}
private fun loadConversations(
offset: Int = 0,
count: Int = DEFAULT_CONVERSATIONS_COUNT,
listener: MvpOnLoadListener<Any?>? = null
) {
if (!AndroidUtils.hasConnection()) {
setState(if (adapter.isEmpty()) ListState.EMPTY_NO_INTERNET else ListState.FILLED)
return
} else {
setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
}
repository.loadConversations(offset, count,
object : MvpOnLoadListener<ArrayList<VKConversation>> {
override fun onResponse(response: ArrayList<VKConversation>) {
conversationsCount = VKConversation.conversationsCount
fillAdapter(response, offset)
setState(if (adapter.isEmpty()) ListState.EMPTY else ListState.FILLED)
listener?.onResponse(null)
}
override fun onError(t: Throwable) {
setState(if (adapter.isEmpty()) ListState.EMPTY_ERROR else ListState.FILLED)
listener?.onError(t)
}
})
}
override fun destroy() {
adapter.destroy()
TimeManager.removeOnMinuteChangeListener(this)
}
private fun createAdapter() {
adapter = ConversationsAdapter(recyclerView, arrayListOf()).also {
it.itemClickListener = this
it.itemLongClickListener = this
}
recyclerView.adapter = adapter
}
private fun fillAdapter(conversations: ArrayList<VKConversation>, offset: Int) {
val oldItems = ArrayList(adapter.values)
if (offset > 0) {
adapter.addAll(conversations)
} else {
adapter.updateValues(conversations)
}
adapter.notifyChanges(oldItems)
if (offset == 0) recyclerView.scrollToPosition(0)
}
override fun onItemClick(position: Int) {
openChat(adapter[position])
}
override fun onItemLongClick(position: Int) {
}
override fun onMinuteChange(currentMinute: Int) {
post { adapter.notifyItemRangeChanged(0, adapter.itemCount, ConversationsCallback.DATE) }
}
private fun openChat(conversation: VKConversation) {
TaskManager.execute {
val peerUser = MemoryCache.getUserById(conversation.conversationId)
val peerGroup = MemoryCache.getGroupById(conversation.conversationId)
val extras = Bundle().also {
it.putInt(MessagesActivity.TAG_EXTRA_ID, conversation.conversationId)
it.putString(
MessagesActivity.TAG_EXTRA_TITLE,
VKUtil.getTitle(conversation, peerUser, peerGroup)
)
it.putString(
MessagesActivity.TAG_EXTRA_AVATAR,
VKUtil.getAvatar(conversation, peerUser, peerGroup)
)
it.putSerializable(MessagesActivity.TAG_EXTRA_USER, peerUser)
it.putSerializable(MessagesActivity.TAG_EXTRA_GROUP, peerGroup)
}
post { viewState.openChat(extras) }
}
}
}
@@ -0,0 +1,232 @@
package com.meloda.fast.fragment.ui.presenter
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.meloda.fast.activity.MessagesActivity
import com.meloda.fast.adapter.UsersAdapter
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.fragment.ui.repository.FriendsRepository
import com.meloda.fast.fragment.ui.view.FriendsView
import com.meloda.fast.listener.ItemClickListener
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.ArrayUtils
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpPresenter
class FriendsPresenter(viewState: FriendsView) :
MvpPresenter<VKUser, FriendsRepository, FriendsView>(
viewState,
FriendsRepository::class.java.name
),
ItemClickListener {
companion object {
const val ONLY_ONLINE = "_only_online"
const val DEFAULT_FRIENDS_COUNT = 30
}
private var userId: Int = 0
private var friendsCount: Int = 0
private lateinit var adapter: UsersAdapter
private lateinit var recyclerView: RecyclerView
private lateinit var layoutManager: LinearLayoutManager
fun setup(userId: Int, recyclerView: RecyclerView, refreshLayout: SwipeRefreshLayout) {
this.userId = userId
this.recyclerView = recyclerView
this.context = recyclerView.context
this.layoutManager = recyclerView.layoutManager as LinearLayoutManager
setRecyclerViewScrollListener(recyclerView)
setRefreshListener(refreshLayout)
createAdapter()
getCachedFriends(userId, 0, DEFAULT_FRIENDS_COUNT, false, object : MvpOnLoadListener<Any?> {
override fun onResponse(response: Any?) {
setState(if (adapter.isEmpty()) MvpPresenter.ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
loadFriends(userId, 0, ConversationsPresenter.DEFAULT_CONVERSATIONS_COUNT)
}
override fun onError(t: Throwable) {
setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
loadFriends(userId, 0, ConversationsPresenter.DEFAULT_CONVERSATIONS_COUNT)
}
})
}
private fun getCachedFriends(
userId: Int,
offset: Int = 0,
count: Int = DEFAULT_FRIENDS_COUNT,
onlyOnline: Boolean = false,
listener: MvpOnLoadListener<Any?>? = null
) {
setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
repository.getCachedFriends(
userId,
offset,
count,
onlyOnline,
object : MvpOnLoadListener<ArrayList<VKUser>> {
override fun onResponse(response: ArrayList<VKUser>) {
val friends = ArrayUtils.cut(response, offset, count)
fillAdapter(friends, offset)
setState(if (adapter.isEmpty()) ListState.EMPTY else ListState.FILLED)
listener?.onResponse(null)
}
override fun onError(t: Throwable) {
setState(if (adapter.isEmpty()) ListState.EMPTY_ERROR else ListState.FILLED)
listener?.onError(t)
}
})
}
private fun loadFriends(
userId: Int,
offset: Int = 0,
count: Int = DEFAULT_FRIENDS_COUNT,
onlyOnline: Boolean = false,
listener: MvpOnLoadListener<Any?>? = null
) {
if (!AndroidUtils.hasConnection()) {
setState(if (adapter.isEmpty()) ListState.EMPTY_NO_INTERNET else ListState.FILLED)
return
} else {
setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING)
}
repository.loadFriends(
userId,
offset,
count,
object : MvpOnLoadListener<ArrayList<VKUser>> {
override fun onResponse(response: ArrayList<VKUser>) {
friendsCount = VKUser.friendsCount
fillAdapter(response, offset)
setState(if (adapter.isEmpty()) ListState.EMPTY else ListState.FILLED)
listener?.onResponse(null)
}
override fun onError(t: Throwable) {
setState(if (adapter.isEmpty()) ListState.EMPTY_ERROR else ListState.FILLED)
listener?.onError(t)
}
})
}
private fun setRecyclerViewScrollListener(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
if (adapter.isLastItem() && !adapter.isLoading && adapter.itemCount < friendsCount) {
adapter.isLoading = true
val position = adapter.itemCount - 1
// adapter.itemCount - 1 - (layoutManager.findLastCompletelyVisibleItemPosition() - layoutManager.findFirstCompletelyVisibleItemPosition())
setState(ListState.FILLED_LOADING)
if (AndroidUtils.hasConnection()) {
loadFriends(
userId,
adapter.itemCount,
DEFAULT_FRIENDS_COUNT,
false,
object : MvpOnLoadListener<Any?> {
override fun onResponse(response: Any?) {
recyclerView.scrollToPosition(position)
adapter.isLoading = false
}
override fun onError(t: Throwable) {
viewState.showErrorSnackbar(t)
}
})
} else {
getCachedFriends(
userId,
adapter.itemCount,
DEFAULT_FRIENDS_COUNT,
false,
object : MvpOnLoadListener<Any?> {
override fun onResponse(response: Any?) {
recyclerView.scrollToPosition(position)
adapter.isLoading = false
}
override fun onError(t: Throwable) {
viewState.showErrorSnackbar(t)
}
})
}
Log.d("RecyclerView", "Bottom reached")
}
}
}
})
}
private fun setRefreshListener(refreshLayout: SwipeRefreshLayout) {
refreshLayout.setOnRefreshListener { loadFriends(userId) }
}
private fun createAdapter() {
adapter = UsersAdapter(context!!, arrayListOf()).also {
it.itemClickListener = this
}
recyclerView.adapter = adapter
}
private fun fillAdapter(values: ArrayList<VKUser>, offset: Int) {
val oldItems = ArrayList(adapter.values)
if (offset > 0) {
adapter.addAll(values)
} else {
adapter.updateValues(values)
}
// adapter.notifyDataSetChanged()
adapter.notifyChanges(oldItems)
if (offset == 0) recyclerView.scrollToPosition(0)
}
private fun openChat(position: Int) {
val user = adapter[position]
val data = Bundle().apply {
putInt(MessagesActivity.TAG_EXTRA_ID, user.userId)
putString(MessagesActivity.TAG_EXTRA_TITLE, user.toString())
putString(MessagesActivity.TAG_EXTRA_AVATAR, user.photo200)
}
}
override fun onItemClick(position: Int) {
}
}
@@ -0,0 +1,186 @@
package com.meloda.fast.fragment.ui.presenter
import android.content.Context
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResultListener
import com.google.android.material.textfield.TextInputEditText
import com.meloda.fast.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.extensions.FragmentExtensions.runOnUiThread
import com.meloda.fast.fragment.FragmentConversations
import com.meloda.fast.fragment.LoginFragment
import com.meloda.fast.fragment.ValidationFragment
import com.meloda.fast.fragment.ui.repository.LoginRepository
import com.meloda.fast.fragment.ui.view.LoginView
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpPresenter
import com.squareup.picasso.Picasso
import org.json.JSONObject
class LoginPresenter(
viewState: LoginView
) : MvpPresenter<Any, LoginRepository, LoginView>(
viewState,
LoginRepository::class.java.name
) {
private var lastEmail: String = ""
private var lastPassword: String = ""
private lateinit var fragment: LoginFragment
fun onCreate(context: Context, fragment: LoginFragment, bundle: Bundle?) {
super.onCreate(context, bundle)
this.fragment = fragment
}
override fun onViewCreated(bundle: Bundle?) {
viewState.initViews()
viewState.prepareViews()
}
fun login(
email: String,
password: String,
captcha: String = "",
onLoadListener: MvpOnLoadListener<Any?>? = null
) {
lastEmail = email
lastPassword = password
repository.login(requireContext(), email, password, captcha,
object : MvpOnLoadListener<JSONObject> {
override fun onResponse(response: JSONObject) {
checkResponse(response, onLoadListener)
}
override fun onError(t: Throwable) {
onLoadListener?.onError(t)
}
})
}
@Suppress("MoveVariableDeclarationIntoWhen")
private fun checkResponse(
response: JSONObject,
onLoadListener: MvpOnLoadListener<Any?>? = null
) {
if (response.has("error")) {
val errorString = response.optString("error")
when (errorString) {
"need_validation" -> {
val redirectUrl = response.optString("redirect_uri")
val bundle = Bundle()
bundle.putString("url", redirectUrl)
fragment.runOnUiThread {
fragment.setFragmentResultListener("validation") { _, bundle ->
val userId = bundle.getInt("userId")
val token = bundle.getString("token") ?: ""
saveUserData(userId, token)
openMainScreen()
}
}
fragment.parentFragmentManager.beginTransaction()
.replace(
R.id.fragmentContainer,
ValidationFragment().apply { arguments = bundle })
.addToBackStack("")
.commit()
}
"need_captcha" -> {
val captchaImage = response.optString("captcha_img")
val captchaSid = response.optString("captcha_sid")
showCaptchaDialog(captchaImage, captchaSid)
}
}
} else {
val userId = response.optInt("user_id", -1)
val token = response.optString("access_token")
saveUserData(userId, token)
openMainScreen()
onLoadListener?.onResponse(null)
}
}
private fun openMainScreen() {
fragment.runOnUiThread {
(fragment.requireActivity() as MainActivity).bottomBar.isVisible = true
fragment.parentFragmentManager.beginTransaction()
.replace(
R.id.fragmentContainer,
FragmentConversations()
).commit()
}
}
private fun saveUserData(userId: Int, token: String) {
UserConfig.userId = userId
UserConfig.token = token
UserConfig.save()
}
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) {
val resources = fragment.resources
val metrics = resources.displayMetrics
fragment.runOnUiThread {
val image = ImageView(requireContext())
image.layoutParams = ViewGroup.LayoutParams(
(metrics.widthPixels / 3.5).toInt(), metrics.heightPixels / 7
)
Picasso.get().load(captchaImage).priority(Picasso.Priority.HIGH).into(image)
val captchaCodeEditText = TextInputEditText(requireContext())
captchaCodeEditText.setHint(R.string.captcha_hint)
captchaCodeEditText.layoutParams =
LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val builder = AlertDialog.Builder(requireContext())
val layout = LinearLayout(requireContext())
layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER
layout.addView(image)
layout.addView(captchaCodeEditText)
builder.setView(layout)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
val captchaCode = captchaCodeEditText.text.toString().trim()
login(
lastEmail,
lastPassword,
"&captcha_sid=$captchaSid&captcha_key=$captchaCode"
)
}
builder.setTitle(R.string.input_captcha)
builder.show()
}
}
}
@@ -0,0 +1,106 @@
package com.meloda.fast.fragment.ui.repository
import com.meloda.fast.api.VKApi
import com.meloda.fast.api.model.VKConversation
import com.meloda.fast.api.model.VKMessage
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.api.util.VKUtil
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.extensions.ArrayExtensions.asArrayList
import com.meloda.fast.listener.OnResponseListener
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpRepository
class ConversationsRepository : MvpRepository<VKConversation>() {
fun loadConversations(
offset: Int, count: Int,
listener: MvpOnLoadListener<ArrayList<VKConversation>>
) {
TaskManager.execute {
VKApi.messages()
.getConversations()
.filter("all")
.extended(true)
.fields(VKUser.DEFAULT_FIELDS)
.offset(offset)
.count(count)
.executeArray(VKConversation::class.java,
object : OnResponseListener<ArrayList<VKConversation>> {
override fun onResponse(response: ArrayList<VKConversation>) {
TaskManager.execute {
cacheLoadedConversations(response)
MemoryCache.putUsers(VKConversation.profiles)
MemoryCache.putGroups(VKConversation.groups)
sendResponse(listener, response)
}
}
override fun onError(t: Throwable) {
sendError(listener, t)
}
})
}
}
fun getCachedConversations(
offset: Int, count: Int,
listener: MvpOnLoadListener<ArrayList<VKConversation>>
) {
if (true) {
sendResponse(listener, arrayListOf())
return
}
TaskManager.execute {
val conversations = MemoryCache.getConversations().asArrayList()
VKUtil.sortConversationsByDate(conversations, true)
sendResponse(listener, conversations)
}
}
private fun fillConversationsWithProfilesAndGroups(conversations: ArrayList<VKConversation>) {
for (conversation in conversations) {
val lastMessage = conversation.lastMessage
when (conversation.type) {
VKConversation.TYPE_USER -> {
VKUtil.searchUser(conversation.conversationId)?.let {
conversation.peerUser = it
}
}
VKConversation.TYPE_GROUP -> {
VKUtil.searchGroup(conversation.conversationId)?.let {
conversation.peerGroup = it
}
}
}
if (lastMessage.isFromGroup()) {
VKUtil.searchGroup(lastMessage.fromId)?.let {
lastMessage.fromGroup = it
}
} else {
VKUtil.searchUser(lastMessage.fromId)?.let {
lastMessage.fromUser = it
}
}
}
}
private fun cacheLoadedConversations(conversations: List<VKConversation>) {
val messages = arrayListOf<VKMessage>()
for (conversation in conversations) {
messages.add(conversation.lastMessage)
}
MemoryCache.putMessages(messages)
MemoryCache.putConversations(conversations)
}
}
@@ -0,0 +1,89 @@
package com.meloda.fast.fragment.ui.repository
import android.util.Log
import com.meloda.fast.api.VKApi
import com.meloda.fast.api.model.VKFriend
import com.meloda.fast.api.model.VKUser
import com.meloda.fast.common.TaskManager
import com.meloda.fast.database.MemoryCache
import com.meloda.fast.listener.OnResponseListener
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpRepository
class FriendsRepository : MvpRepository<VKUser>() {
fun loadFriends(
userId: Int,
offset: Int,
count: Int,
listener: MvpOnLoadListener<ArrayList<VKUser>>
) {
TaskManager.execute {
VKApi.friends()
.get()
.order("hints")
.userId(userId)
.fields(VKUser.DEFAULT_FIELDS)
.count(count)
.offset(offset)
.executeArray(VKUser::class.java,
object : OnResponseListener<ArrayList<VKUser>> {
override fun onResponse(response: ArrayList<VKUser>) {
Log.d("FriendsRepository", "get ${response.size} friends from api")
TaskManager.execute {
cacheLoadedUsers(userId, response)
}
sendResponse(listener, response)
}
override fun onError(t: Throwable) {
sendError(listener, t)
}
})
}
}
fun getCachedFriends(
userId: Int, offset: Int, count: Int, onlyOnline: Boolean,
listener: MvpOnLoadListener<ArrayList<VKUser>>
) {
TaskManager.execute {
val friendsArray = MemoryCache.getFriends(userId)
Log.d("FriendsRepository", "get ${friendsArray.size} friends from cache")
if (friendsArray.isEmpty()) {
sendError(listener, NullPointerException("Friends list is empty"))
return@execute
}
val friends = arrayListOf<VKUser>()
for (friend in friendsArray) {
val user = MemoryCache.getUserById(friend.friendId)
user?.let {
if (onlyOnline && user.isOnline || !onlyOnline) {
friends.add(user)
}
}
}
sendResponse(listener, friends)
}
}
private fun cacheLoadedUsers(userId: Int, users: ArrayList<VKUser>) {
MemoryCache.putUsers(users)
val friends = ArrayList<VKFriend>()
for (user in users) {
friends.add(VKFriend(user.userId, userId))
}
MemoryCache.putFriends(friends)
}
}
@@ -0,0 +1,74 @@
package com.meloda.fast.fragment.ui.repository
import android.annotation.SuppressLint
import android.content.Context
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import com.meloda.fast.api.VKAuth
import com.meloda.mvp.MvpOnLoadListener
import com.meloda.mvp.MvpRepository
import org.json.JSONObject
import org.jsoup.Jsoup
class LoginRepository : MvpRepository<Any>() {
fun login(
context: Context,
email: String,
password: String,
captcha: String,
onLoadListener: MvpOnLoadListener<JSONObject>
) {
if (email.trim().isEmpty() || password.trim().isEmpty()) return
val loadingUrl = VKAuth.getDirectAuthUrl(email, password, captcha)
val webView = createWebView(context)
webView.addJavascriptInterface(WebViewHandlerInterface(onLoadListener), "HtmlHandler")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webView.loadUrl(
"javascript:window.HtmlHandler.handleHtml" +
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');"
)
}
}
webView.loadUrl(loadingUrl)
}
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(context: Context): WebView {
val loginWebView = WebView(context)
loginWebView.settings.javaScriptEnabled = true
loginWebView.settings.domStorageEnabled = true
loginWebView.settings.loadsImagesAutomatically = false
loginWebView.settings.userAgentString = "Chrome/41.0.2228.0 Safari/537.36"
loginWebView.clearCache(true)
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
cookieManager.flush()
cookieManager.setAcceptCookie(false)
return loginWebView
}
private class WebViewHandlerInterface(private var onLoadListener: MvpOnLoadListener<JSONObject>) {
@JavascriptInterface
fun handleHtml(html: String?) {
val doc = Jsoup.parse(html)
val responseString =
doc.select("pre[style=\"word-wrap: break-word; white-space: pre-wrap;\"]")
.first()
.text()
onLoadListener.onResponse(JSONObject(responseString))
}
}
}
@@ -0,0 +1,10 @@
package com.meloda.fast.fragment.ui.view
import android.os.Bundle
import com.meloda.mvp.MvpView
interface ConversationsView : MvpView {
fun openChat(extras: Bundle)
}

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