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)
}