diff --git a/.gitignore b/.gitignore index dcdc776b..aa724b77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,83 +1,15 @@ -# Built application files -*.apk -*.ap_ -*.aab - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ -out/ -# Uncomment the following line in case you need and you don't have the release build type files in your app -# release/ - -# Gradle files -.gradle/ -build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Proguard folder generated by Eclipse -proguard/ - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder -captures/ - -# IntelliJ *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -# Android Studio 3 in .gitignore file. -.idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml - -# Keystore files -# Uncomment the following lines if you do not want to check your keystore files in. -#*.jks -#*.keystore - -# External native build folder generated in Android Studio 2.2 and later +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures .externalNativeBuild - -# Google Services (e.g. APIs or Firebase) -# google-services.json - -# Freeline -freeline.py -freeline/ -freeline_project_description.json - -# fastlane -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output -fastlane/readme.md - -# Version control -vcs.xml - -# lint -lint/intermediates/ -lint/generated/ -lint/outputs/ -lint/tmp/ -# lint/reports/ +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..0691aa87 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,72 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "ru.melod1n.project.vkm" + minSdkVersion 23 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + coreLibraryDesugaringEnabled true + + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + + implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + + implementation 'androidx.core:core-ktx:1.5.0-beta01' + + implementation 'androidx.appcompat:appcompat:1.3.0-beta01' + implementation 'androidx.preference:preference-ktx:1.1.1' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01' + implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.fragment:fragment-ktx:1.3.0' + implementation 'com.google.android.material:material:1.3.0' + + implementation 'androidx.room:room-runtime:2.3.0-beta01' + kapt 'androidx.room:room-compiler:2.3.0-beta01' + + implementation 'com.facebook.fresco:fresco:2.3.0' + + implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' + + implementation 'com.squareup.picasso:picasso:2.71828' + + implementation 'com.github.rahatarmanahmed:circularprogressview:2.5.0' + + implementation 'com.google.code.gson:gson:2.8.6' + + implementation 'org.jsoup:jsoup:1.13.1' + + implementation 'ch.acra:acra:4.11.1' + + def appCenterSdkVersion = '4.1.0' + implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" + implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" + implementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b53e3372 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..3370f64a Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt new file mode 100644 index 00000000..d9052672 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/DropUserDataActivity.kt @@ -0,0 +1,23 @@ +package ru.melod1n.project.vkm.activity + +import android.content.Intent +import android.os.Bundle +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.api.UserConfig + +class DropUserDataActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + UserConfig.clear() + + TaskManager.execute { AppGlobal.database.clearAllTables() } + + startActivity(Intent(this, MainActivity::class.java)) + finishAffinity() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt new file mode 100644 index 00000000..9905716b --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/LoginActivity.kt @@ -0,0 +1,134 @@ +package ru.melod1n.project.vkm.activity + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.webkit.* +import android.widget.ProgressBar +import androidx.core.view.isVisible +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKAuth +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.widget.Toolbar + +class LoginActivity : BaseActivity() { + + private lateinit var toolbar: Toolbar + private lateinit var progressBar: ProgressBar + private lateinit var webView: WebView + private lateinit var refreshLayout: SwipeRefreshLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + initViews() + + prepareToolbar() + prepareRefreshLayout() + + prepareSettings() + + val url = VKAuth.getUrl(UserConfig.API_ID, VKAuth.settings) + + webView.loadUrl(url) + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + progressBar = findViewById(R.id.progressBar) + webView = findViewById(R.id.webView) + refreshLayout = findViewById(R.id.refreshLayout) + } + + @SuppressLint("SetJavaScriptEnabled") + private fun prepareSettings() { + webView.settings.javaScriptEnabled = true + webView.clearCache(true) + webView.webViewClient = VKWebClient() + + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) + } + + private fun prepareToolbar() { + setSupportActionBar(toolbar) + + toolbar.navigationIcon = drawable(R.drawable.ic_close).tint(color(R.color.accent)) + toolbar.setNavigationClickListener { onBackPressed() } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) onBackPressed() + return super.onOptionsItemSelected(item) + } + + private fun prepareRefreshLayout() { + refreshLayout.apply { + setColorSchemeColors(color(R.color.accent)) + setOnRefreshListener { + webView.reload() + isRefreshing = false + } + } + } + + private inner class VKWebClient : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + view.loadUrl(url) + return true + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + progressBar.isVisible = true + view.isVisible = false + + parseUrl(url) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + progressBar.isVisible = false + view.isVisible = true + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest?, + error: WebResourceError? + ) { + super.onReceivedError(view, request, error) + Log.e("VKM WebView", error.toString()) + } + } + + private fun parseUrl(url: String) { + try { + if (url.startsWith(VKAuth.redirectUrl) && !url.contains("error=")) { + val auth = VKAuth.parseRedirectUrl(url) + val token = auth[0] + val id = auth[1].toInt() + + UserConfig.token = token + UserConfig.userId = id + UserConfig.save() + + finishAffinity() + startActivity(Intent(this, MainActivity::class.java)) + } + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt new file mode 100644 index 00000000..24752f82 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/MainActivity.kt @@ -0,0 +1,260 @@ +package ru.melod1n.project.vkm.activity + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.core.view.GravityCompat +import androidx.core.view.isVisible +import androidx.drawerlayout.widget.DrawerLayout +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationView +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.FragmentSwitcher +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.common.TimeManager +import ru.melod1n.project.vkm.dialog.AccountDialog +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.fragment.FragmentConversations +import ru.melod1n.project.vkm.fragment.FragmentFriends +import ru.melod1n.project.vkm.fragment.FragmentSettings +import ru.melod1n.project.vkm.fragment.LoginFragment +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.service.LongPollService +import ru.melod1n.project.vkm.util.AndroidUtils +import ru.melod1n.project.vkm.util.ViewUtils +import ru.melod1n.project.vkm.widget.Toolbar + + +class MainActivity : BaseActivity(), + NavigationView.OnNavigationItemSelectedListener, + BottomNavigationView.OnNavigationItemSelectedListener { + + private lateinit var fragmentConversations: FragmentConversations + private lateinit var fragmentFriends: FragmentFriends + private lateinit var fragmentSettings: FragmentSettings + + private var selectedId = 0 + + private lateinit var drawerLayout: DrawerLayout + lateinit var bottomBar: BottomNavigationView + private lateinit var navigationView: NavigationView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + initViews() + +// checkLogin() + + if (UserConfig.isLoggedIn()) { + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, FragmentConversations()) + .commit() + } else { + bottomBar.isVisible = false + + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, LoginFragment()) + .commit() + } + + +// TimeManager.init(this) + +// prepareFragments() + +// prepareNavigationView() +// prepareBottomBar() +// checkLogin() + } + + private fun initViews() { + drawerLayout = findViewById(R.id.drawerLayout) + bottomBar = findViewById(R.id.bottomBar) + navigationView = findViewById(R.id.navigationView) + } + + override fun onDestroy() { + TimeManager.destroy() + super.onDestroy() + } + + private fun prepareFragments() { + fragmentConversations = FragmentConversations() + fragmentFriends = FragmentFriends(UserConfig.userId) + fragmentSettings = FragmentSettings() + + val containerId = R.id.fragmentContainer + + FragmentSwitcher.addFragments( + supportFragmentManager, + containerId, + listOf(fragmentConversations) + ) + } + + fun initToolbar(toolbar: Toolbar) { + toolbar.navigationIcon = + drawable(R.drawable.ic_search).tint(color(R.color.text_secondary_60_alpha)) + + toolbar.setTitleMode(Toolbar.TitleMode.HINT) + toolbar.setTitle(R.string.action_search) + toolbar.setAvatarClickListener { openAccountDialog() } + } + + private fun openAccountDialog() { + AccountDialog().show(supportFragmentManager, AccountDialog.TAG) + } + + private fun prepareNavigationView() { + navigationView.layoutParams?.width = AppGlobal.screenWidth - AppGlobal.screenWidth / 6 + + navigationView.setNavigationItemSelectedListener(this) + + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + + private fun prepareBottomBar() { +// val menu = bottomBar.menu +// +// val navigationFriends = menu.add(R.string.navigation_friends) +// navigationFriends.icon = drawable(R.drawable.ic_people_outline) +// +// val navigationConversations = menu.add(R.string.navigation_conversations) +// navigationConversations.icon = drawable(R.drawable.ic_message_outline) +// +// val navigationImportant = menu.add(R.string.navigation_important) +// navigationImportant.icon = drawable(R.drawable.ic_star_border) + + bottomBar.setOnNavigationItemSelectedListener(this) + } + + private fun createMenuItem(menu: Menu, tag: String): MenuItem { + return when (tag) { + "friends" -> + menu.add("Friends").apply { icon = drawable(R.drawable.ic_people_outline) } + "conversations" -> + menu.add("Conversations").apply { icon = drawable(R.drawable.ic_message_outline) } + "important" -> + menu.add("Important").apply { icon = drawable(R.drawable.ic_star_border) } + + else -> menu.add("") + } + } + + private fun checkLogin() { + if (UserConfig.isLoggedIn()) { + startLongPoll() + loadProfileInfo() + } else { + openStartScreen() + } + } + + private fun openMainScreen() { + selectedId = R.id.navigationConversations + bottomBar.selectedItemId = selectedId + openConversationsScreen() + } + + private fun startLongPoll() { + startService(Intent(this, LongPollService::class.java)) + } + + private fun openStartScreen() { + finish() + startActivity(Intent(this, StartActivity::class.java)) + } + + private fun openConversationsScreen() { + FragmentSwitcher.showFragment( + supportFragmentManager, + fragmentConversations.javaClass.simpleName, + true + ) + } + + private fun openFriendsScreen() { + FragmentSwitcher.showFragment( + supportFragmentManager, + fragmentFriends.javaClass.simpleName, + true + ) + } + + private fun openSettingsScreen() { + startActivity(Intent(this, SettingsActivity::class.java)) + } + + private fun loadProfileInfo() { + if (AndroidUtils.hasConnection()) { + TaskManager.loadUser(VKApiKeys.UPDATE_USER, UserConfig.userId, + object : OnResponseListener { + override fun onResponse(response: VKUser) { + prepareNavigationHeader(response) + openMainScreen() + } + + override fun onError(t: Throwable) { + openMainScreen() + } + }) + } + } + + private fun prepareNavigationHeader(user: VKUser) { + ViewUtils.prepareNavigationHeader(navigationView.getHeaderView(0), user) + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + switchFragment(item.itemId) + return true + } + + private fun switchFragment(itemId: Int) { + var valid = true + + when (itemId) { + R.id.navigationConversations -> { + openConversationsScreen() + } + R.id.navigationFriends -> { + openFriendsScreen() + } + R.id.navigationSettings -> { + openSettingsScreen() + } + else -> { + valid = false + } + } + + if (!valid) return + + if (selectedId != itemId) { + selectedId = itemId + navigationView.setCheckedItem(selectedId) + } + + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START) + } + } + + override fun onBackPressed() { + if (drawerLayout.isDrawerOpen(navigationView)) { + drawerLayout.closeDrawer(navigationView) + } else { + super.onBackPressed() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt new file mode 100644 index 00000000..1b6ef652 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/MessagesActivity.kt @@ -0,0 +1,403 @@ +package ru.melod1n.project.vkm.activity + +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.amulyakhare.textdrawable.TextDrawable +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.ui.presenter.MessagesPresenter +import ru.melod1n.project.vkm.activity.ui.view.MessagesView +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKGroup +import ru.melod1n.project.vkm.api.model.VKModel +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.dialog.ProfileDialog +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.extensions.ImageViewExtensions.loadImage +import ru.melod1n.project.vkm.fragment.FragmentSettings +import ru.melod1n.project.vkm.util.KeyboardUtils +import ru.melod1n.project.vkm.util.TextUtils +import ru.melod1n.project.vkm.util.ViewUtils +import ru.melod1n.project.vkm.widget.CircleImageView + + +class MessagesActivity : BaseActivity(), MessagesView { + + companion object { + const val TAG = "MessagesActivity" + + const val MESSAGES_COUNT = 30 + + const val TAG_EXTRA_TITLE = "title" + const val TAG_EXTRA_AVATAR = "avatar" + const val TAG_EXTRA_ID = "id" + const val TAG_EXTRA_USER = "user" + const val TAG_EXTRA_GROUP = "group" + } + + private var isEdit = false + + private var fabState = FabState.VOICE + + private enum class FabState { + VOICE, SEND, EDIT, DELETE, BLOCKED + } + + private var title = "" + private var avatar = "" + + private var lastMessageText = "" + private var attachments = arrayListOf() + + private var peerId = 0 + + private var dialogUser: VKUser? = null + private var dialogGroup: VKGroup? = null + + private lateinit var presenter: MessagesPresenter + + lateinit var recyclerView: RecyclerView + private lateinit var refreshLayout: SwipeRefreshLayout + private lateinit var toolbar: Toolbar + private lateinit var chatAvatar: CircleImageView + private lateinit var chatTitle: TextView + private lateinit var chatInfo: TextView + private lateinit var chatPanel: LinearLayout + private lateinit var chatMessage: EditText + private lateinit var chatSend: ImageButton + private lateinit var progressBar: ProgressBar + + private lateinit var noItemsView: LinearLayout + private lateinit var noInternetView: LinearLayout + private lateinit var errorView: LinearLayout + + override fun onDestroy() { + super.onDestroy() + presenter.destroy() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_messages) + initViews() + + initExtraData() + + prepareToolbar() + prepareRefreshLayout() + prepareRecyclerView() + prepareEditText() + + presenter = MessagesPresenter(this) + presenter.setup(peerId, recyclerView) + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + recyclerView = findViewById(R.id.recyclerView) + refreshLayout = findViewById(R.id.refreshLayout) + chatAvatar = findViewById(R.id.chatAvatar) + chatTitle = findViewById(R.id.chatTitle) + chatInfo = findViewById(R.id.chatInfo) + chatPanel = findViewById(R.id.chatPanel) + chatMessage = findViewById(R.id.chatMessage) + chatSend = findViewById(R.id.chatSend) + progressBar = findViewById(R.id.progressBar) + + noItemsView = findViewById(R.id.noItemsView) + noInternetView = findViewById(R.id.noInternetView) + errorView = findViewById(R.id.errorView) + } + + private fun initExtraData() { + peerId = intent.getIntExtra(TAG_EXTRA_ID, -1) + title = intent.getStringExtra(TAG_EXTRA_TITLE) ?: "" + avatar = intent.getStringExtra(TAG_EXTRA_AVATAR) ?: "" + + dialogUser = intent.getSerializableExtra(TAG_EXTRA_USER) as VKUser? + dialogGroup = intent.getSerializableExtra(TAG_EXTRA_GROUP) as VKGroup? + } + + private fun prepareToolbar() { + setSupportActionBar(toolbar) + + val placeholder = TextDrawable + .builder() + .buildRound(TextUtils.getFirstLetterFromString(title), color(R.color.accent)) + + chatAvatar.setImageDrawable(placeholder) + + chatAvatar.loadImage(avatar, placeholder) + + toolbar.setOnClickListener { presenter.openProfile() } + + chatAvatar.setOnClickListener { presenter.openProfile() } + + chatTitle.text = title + + supportActionBar?.setDisplayShowTitleEnabled(false) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + toolbar.navigationIcon.tint(color(R.color.accent)) + } + + private fun prepareRefreshLayout() { + refreshLayout.isEnabled = false + } + + private fun prepareRecyclerView() { + recyclerView.layoutManager = + LinearLayoutManager(this, RecyclerView.VERTICAL, false).also { + it.stackFromEnd = true + } + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + if (dy < 0 && AppGlobal.inputMethodManager.isAcceptingText && AppGlobal.preferences.getBoolean( + FragmentSettings.KEY_HIDE_KEYBOARD_ON_SCROLL_UP, + true + ) + ) { + KeyboardUtils.hideKeyboardFrom(chatMessage) + } + } + }) + } + + private fun prepareEditText() { + chatMessage.addTextChangedListener { + fabState = if (it.toString().trim().isEmpty()) { + if (isEdit) { + FabState.DELETE + } else { + FabState.VOICE + } + } else { + if (isEdit) { + FabState.EDIT + } else { + FabState.SEND + } + } + + refreshFabStyle() + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_messages, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> onBackPressed() + + R.id.messagesRefresh -> { + presenter.updateData() + } + } + + return super.onOptionsItemSelected(item) + } + + private fun refreshFabStyle() { + chatSend.isClickable = true + + when (fabState) { + FabState.VOICE -> { + chatSend.apply { + setImageResource(R.drawable.ic_mic) + + setOnClickListener { + showVoiceRecordingTip() + } + setOnLongClickListener { + true + } + } + } + FabState.SEND -> { + chatSend.apply { + setImageResource(R.drawable.ic_send) + + setOnClickListener { + presenter.sendMessage(chatMessage.text.toString(), attachments) + } + + setOnLongClickListener { + presenter.sendMessage(chatMessage.text.toString(), attachments, false) + true + } + } + } + FabState.EDIT -> { + chatSend.apply { + setImageResource(R.drawable.ic_done) + + setOnClickListener { + //editMessage() + } + + setOnLongClickListener { + performClick() + true + } + } + + } + FabState.DELETE -> { + chatSend.apply { + setImageResource(R.drawable.ic_trash_outline) + + chatSend.setOnClickListener { + //deleteMessage + } + + chatSend.setOnLongClickListener { + performClick() + true + } + } + } + FabState.BLOCKED -> { + chatSend.apply { + isClickable = false + setImageResource(R.drawable.ic_lock) + } + } + } + } + + override fun showChatPanel() { + chatPanel.isVisible = true + } + + override fun hideChatPanel() { + chatPanel.isVisible = false + } + + override fun setWritingAllowed(allowed: Boolean) { + if (allowed) { + fabState = FabState.VOICE + + chatSend.imageTintList = ColorStateList.valueOf(color(R.color.accent)) + + chatMessage.isEnabled = true + + chatPanel.setBackgroundResource(R.drawable.chat_panel_background) + } else { + fabState = FabState.BLOCKED + + chatSend.imageTintList = ColorStateList.valueOf(Color.WHITE) + + chatMessage.isEnabled = false + chatMessage.setHintTextColor(Color.WHITE) + chatMessage.setHint(R.string.no_access) + + chatPanel.setBackgroundResource(R.drawable.chat_panel_background_blocked) + } + } + + override fun setChatInfo(info: String) { + chatInfo.text = info + chatInfo.isVisible = info.isNotEmpty() + } + + override fun openProfile(conversation: VKConversation) { + conversation.let { + val profileDialog = ProfileDialog(it, title) + profileDialog.show(supportFragmentManager, ProfileDialog.TAG) + } + } + + override fun showErrorLoadConversationAlert() { + val builder = AlertDialog.Builder(this) + builder.setTitle(R.string.error_occurred) + builder.setMessage(R.string.error_loading_message) + builder.setPositiveButton(R.string.retry) { _, _ -> + presenter.loadConversation(peerId) + } + builder.setNegativeButton(R.string.no) { _, _ -> onBackPressed() } + builder.setCancelable(false) + builder.show() + } + + override fun showVoiceRecordingTip() { + Toast.makeText(this, R.string.voice_record_tip, Toast.LENGTH_LONG).show() + } + + override fun setMessageText(text: String) { + chatMessage.setText(text) + } + + override fun showErrorSnackbar(t: Throwable) { + ViewUtils.showErrorSnackbar(getRootView(), t) + } + + override fun prepareNoItemsView() { + } + + override fun showNoItemsView() { + noItemsView.isVisible = true + } + + override fun hideNoItemsView() { + noItemsView.isVisible = false + } + + override fun prepareNoInternetView() { + } + + override fun showNoInternetView() { + noInternetView.isVisible = true + } + + override fun hideNoInternetView() { + noInternetView.isVisible = false + } + + override fun prepareErrorView() { + } + + override fun showErrorView() { + errorView.isVisible = true + } + + override fun hideErrorView() { + errorView.isVisible = false + } + + override fun showProgressBar() { + progressBar.isVisible = true + } + + override fun hideProgressBar() { + progressBar.isVisible = false + } + + override fun showRefreshLayout() { + refreshLayout.isRefreshing = true + } + + override fun hideRefreshLayout() { + refreshLayout.isRefreshing = false + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt new file mode 100644 index 00000000..e2711beb --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/SettingsActivity.kt @@ -0,0 +1,44 @@ +package ru.melod1n.project.vkm.activity + +import android.os.Bundle +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.FragmentSwitcher +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.fragment.FragmentSettings +import ru.melod1n.project.vkm.widget.Toolbar + +class SettingsActivity : BaseActivity() { + + private lateinit var toolbar: Toolbar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + initViews() + + setSupportActionBar(toolbar) + + toolbar.navigationIcon = drawable(R.drawable.ic_arrow_back).tint(color(R.color.accent)) + + toolbar.setNavigationClickListener { onBackPressed() } + + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, FragmentSettings()).commitNow() + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + } + + override fun onBackPressed() { + val currentFragment = FragmentSwitcher.getCurrentFragment(supportFragmentManager) ?: return + + if (currentFragment.javaClass == FragmentSettings::class.java && (currentFragment as FragmentSettings).onBackPressed()) { + super.onBackPressed() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt new file mode 100644 index 00000000..f6fbf3a9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/StartActivity.kt @@ -0,0 +1,82 @@ +package ru.melod1n.project.vkm.activity + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import com.google.android.material.button.MaterialButton +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.base.BaseActivity + +@SuppressLint("InflateParams") +class StartActivity : BaseActivity() { + + private lateinit var startEnter: MaterialButton + private lateinit var startLoginSettings: MaterialButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_start) + initViews() + + prepareEnterButton() + } + + private fun initViews() { + startEnter = findViewById(R.id.startEnter) + startLoginSettings = findViewById(R.id.startLoginSettings) + } + + private fun prepareEnterButton() { + startEnter.setOnClickListener { + startActivity(Intent(this, LoginActivity::class.java)) + } + + startEnter.setOnLongClickListener { + showUserIdTokenDialog() + true + } + + startLoginSettings.setOnClickListener { + Toast.makeText(this, R.string.in_progress_placeholder, Toast.LENGTH_LONG).show() + } + } + + private fun showUserIdTokenDialog() { + AlertDialog.Builder(this).apply { + setTitle(R.string.custom_data) + + val view = LayoutInflater.from(this@StartActivity) + .inflate(R.layout.activity_login_custom_data, null, false) as View + setView(view) + + val userId = view.findViewById(R.id.customDataUserId) + val token = view.findViewById(R.id.customDataToken) + + setPositiveButton(android.R.string.ok) { _, _ -> + if (userId.text.toString().isEmpty() || token.text.toString().isEmpty()) + return@setPositiveButton + val id = userId.text.toString().toInt() + val accessToken = token.text.toString() + + if (id < 1) return@setPositiveButton + + UserConfig.userId = id + UserConfig.token = accessToken + UserConfig.save() + + finish() + startActivity(Intent(this@StartActivity, MainActivity::class.java)) + } + + setCancelable(false) + setNegativeButton(android.R.string.cancel, null) + }.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt new file mode 100644 index 00000000..8d13fe05 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/UpdateActivity.kt @@ -0,0 +1,350 @@ +package ru.melod1n.project.vkm.activity + +import android.app.Activity +import android.app.DownloadManager +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.content.FileProvider +import androidx.core.text.HtmlCompat +import androidx.core.view.isVisible +import com.github.rahatarmanahmed.cpv.CircularProgressView +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.common.UpdateManager +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.FloatExtensions.int +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.model.NewUpdateInfo +import ru.melod1n.project.vkm.receiver.DownloadUpdateReceiver +import ru.melod1n.project.vkm.util.AndroidUtils +import ru.melod1n.project.vkm.util.TimeUtils +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + + +class UpdateActivity : BaseActivity() { + + companion object { + private const val FILE_BASE_PATH = "file://" + private const val MIME_TYPE = "application/vnd.android.package-archive" + private const val PROVIDER_PATH = ".provider" + } + + private var isChecking = false + private var isNewUpdate = false + private var isDownloading = false + + private var downloadId = 0L + + private var lastCheckTime = 0L + + private var newUpdate = NewUpdateInfo() + + private lateinit var updateCheckUpdates: ExtendedFloatingActionButton + private lateinit var updateState: TextView + private lateinit var updateVersion: TextView + private lateinit var updateInfo: TextView + private lateinit var updateInfoLayout: LinearLayout + private lateinit var updateProgress: LinearLayout + private lateinit var updateProgressBar: CircularProgressView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_update) + initViews() + + updateProgressBar.maxProgress = 100F + + lastCheckTime = AppGlobal.preferences.getLong("updateCheckTime", 0) + + refreshState() + + checkUpdates() + + updateCheckUpdates.setOnClickListener { + lastCheckTime = System.currentTimeMillis() + AppGlobal.preferences.edit().putLong("updateCheckTime", lastCheckTime).apply() + + checkUpdates() + } + } + + private fun initViews() { + updateCheckUpdates = findViewById(R.id.updateCheckUpdates) + updateInfo = findViewById(R.id.updateInfo) + updateVersion = findViewById(R.id.updateVersion) + updateState = findViewById(R.id.updateState) + updateInfoLayout = findViewById(R.id.updateInfoLayout) + updateProgress = findViewById(R.id.updateProgress) + updateProgressBar = updateProgress.getChildAt(0) as CircularProgressView + } + + private fun installUpdate(context: Activity, file: File) { + val install = Intent(Intent.ACTION_VIEW) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + install.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) + install.data = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + PROVIDER_PATH, + file + ) + } else { + install.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + install.setDataAndType(Uri.fromFile(file), MIME_TYPE) + } + + context.startActivity(install) +// context.finishAffinity() + } + + private fun downloadUpdate() { + checkIsInstallingAllowed() + + val timer = Timer() + + updateCheckUpdates.shrink() + updateCheckUpdates.isClickable = false + + isDownloading = true + refreshState() + + TaskManager.execute { + val apkName = newUpdate.version + + val destination = + getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/$apkName.apk" + + val uri = Uri.parse("$FILE_BASE_PATH$destination") + + val file = File(destination) + if (file.exists()) file.delete() + + val request = DownloadManager.Request(Uri.parse(newUpdate.downloadLink)) + + request.setTitle("${getString(R.string.app_name)} ${apkName}.apk") + request.setMimeType(MIME_TYPE) + request.setDestinationUri(uri) + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + + val receiver = DownloadUpdateReceiver() + receiver.listener = object : OnResponseListener { + override fun onResponse(response: Any?) { + timer.cancel() + + installUpdate(this@UpdateActivity, file) + + unregisterReceiver(receiver) + + runOnUiThread { + updateProgressBar.isIndeterminate = true + + updateCheckUpdates.extend() + updateCheckUpdates.isClickable = true + + isDownloading = false + refreshState() + } + } + + override fun onError(t: Throwable) { + } + } + + registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + + downloadId = AppGlobal.downloadManager.enqueue(request) + + timer.schedule(object : TimerTask() { + override fun run() { + val query = DownloadManager.Query() + query.setFilterById(downloadId) + + val cursor = AppGlobal.downloadManager.query(query) + if (cursor.moveToFirst()) { + val sizeIndex = + cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + val downloadedIndex = + cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + val size = cursor.getInt(sizeIndex) + val downloaded = cursor.getInt(downloadedIndex) + + val progress = if (size != -1) (downloaded * 100.0F / size) else 0.0F + + Log.d("Downloading update", "progress $progress%") + + if (progress.int() > 0) { + runOnUiThread { + if (updateProgressBar.isIndeterminate) { + updateProgressBar.isIndeterminate = false + updateProgressBar.stopAnimation() + } + + updateProgressBar.progress = progress + } + } + } + } + + }, 0, 1000) + } + } + + private fun checkUpdates() { + if (isChecking) return + + isChecking = true + refreshState() + + UpdateManager.checkUpdates(object : UpdateManager.OnUpdateListener { + override fun onNewUpdate(updateInfo: NewUpdateInfo) { + isChecking = false + isNewUpdate = true + + this@UpdateActivity.newUpdate = updateInfo + + refreshState() + } + + override fun onNoUpdates() { + isNewUpdate = false + isChecking = false + + this@UpdateActivity.newUpdate = NewUpdateInfo() + + refreshState() + } + }) + } + + private fun checkIsInstallingAllowed() { + if (!AndroidUtils.isCanInstallUnknownApps(this)) { + val builder = AlertDialog.Builder(this) + builder.setTitle(R.string.warning) + builder.setMessage(R.string.update_unknown_sources_disabled_message) + builder.setPositiveButton(R.string.yes) { _, _ -> + AndroidUtils.openInstallUnknownAppsScreen(this) + } + builder.setNegativeButton(R.string.no, null) + builder.show() + } + } + + private fun refreshState() { + when { + isChecking -> { + updateState.text = getString(R.string.update_state_checking) + + setAlpha(updateInfoLayout, true) + setAlpha(updateProgress, false) + setAlpha(updateCheckUpdates, true) + } + isDownloading -> { + updateState.text = getString(R.string.update_state_downloading) + + setAlpha(updateInfoLayout, true) + setAlpha(updateProgress, false) + setAlpha(updateCheckUpdates, true) + } + else -> { + if (isNewUpdate) { + updateCheckUpdates.text = getString(R.string.update_download) + updateCheckUpdates.icon = drawable(R.drawable.ic_file_download) + } else { + updateCheckUpdates.text = getString(R.string.update_check_updates) + updateCheckUpdates.icon = drawable(R.drawable.ic_refresh) + } + + updateCheckUpdates.setOnClickListener { + if (isNewUpdate) { + downloadUpdate() + } else { + checkUpdates() + } + } + + updateState.text = + getString(if (isNewUpdate) R.string.update_state_update_available else R.string.update_state_no_updates) + + updateVersion.text = + if (isNewUpdate) + getString( + R.string.update_new_version, + newUpdate.version, + newUpdate.code + ) + else getString( + R.string.update_current_version, + AppGlobal.versionName, + AppGlobal.versionCode + ) + + updateInfo.text = + when { + isNewUpdate -> if (newUpdate.changelog.isEmpty()) "" else getString( + R.string.update_changelog, + HtmlCompat.fromHtml( + newUpdate.changelog, + HtmlCompat.FROM_HTML_MODE_LEGACY + ) + ) + lastCheckTime.toString().isEmpty() || lastCheckTime == 0L -> "" + else -> getString(R.string.update_last_check_time, getCheckTime()) + } + + setAlpha(updateInfoLayout, false) + setAlpha(updateProgress, true) + setAlpha(updateCheckUpdates, false) + } + } + } + + private fun getCheckTime(): String { + val time = lastCheckTime + + val lastTime = TimeUtils.removeTime(Date(time)) + val currentTime = TimeUtils.removeTime(Date(System.currentTimeMillis())) + + val format = if (currentTime > lastTime) { + "dd.MM.yyyy HH:mm" + } else { + "HH:mm" + } + + return SimpleDateFormat(format, Locale.getDefault()).format(time) + } + + private fun setAlpha(view: View, toZero: Boolean) { + if (toZero) { + view.animate() + .alpha(0F) + .setDuration(250) + .withEndAction { view.isVisible = false } + .start() + } else { + view.animate() + .alpha(1F) + .setDuration(250) + .withStartAction { view.isVisible = true } + .start() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt new file mode 100644 index 00000000..377c7250 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/presenter/MessagesPresenter.kt @@ -0,0 +1,255 @@ +package ru.melod1n.project.vkm.activity.ui.presenter + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.ui.repository.MessagesRepository +import ru.melod1n.project.vkm.activity.ui.view.MessagesView +import ru.melod1n.project.vkm.adapter.MessagesAdapter +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.model.VKModel +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpPresenter +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.listener.ItemClickListener +import ru.melod1n.project.vkm.listener.ItemLongClickListener +import kotlin.random.Random + +class MessagesPresenter(viewState: MessagesView) : + MvpPresenter( + viewState, + MessagesRepository::class.java.name + ), + ItemClickListener, + ItemLongClickListener, + TaskManager.OnEventListener { + + companion object { + const val DEFAULT_MESSAGES_COUNT = 30 + } + + private lateinit var adapter: MessagesAdapter + private lateinit var conversation: VKConversation + + private var peerId: Int = -1 + + private var lastMessageText: String = "" + + private lateinit var recyclerView: RecyclerView + + override fun destroy() { + adapter.destroy() + } + + fun setup(peerId: Int, recyclerView: RecyclerView) { + this.peerId = peerId + this.recyclerView = recyclerView + this.context = recyclerView.context + + viewState.showProgressBar() + getCachedConversation(peerId) + } + + fun updateData() { + adapter.clear() + loadMessages(peerId) + } + + fun openProfile() { + viewState.openProfile(conversation) + } + + private fun createAdapter() { + adapter = MessagesAdapter(context!!, arrayListOf(), conversation).also { + it.itemClickListener = this + it.itemLongClickListener = this + } + + recyclerView.adapter = adapter + } + + private fun getCachedConversation(peerId: Int) { + repository.getCachedConversation(peerId, object : MvpOnLoadListener { + override fun onResponse(response: VKConversation) { + conversation = response + + createAdapter() + refreshConversation(response) + + getCachedMessages(peerId, 0, DEFAULT_MESSAGES_COUNT, + object : MvpOnLoadListener { + override fun onResponse(response: Any?) { + loadConversation(peerId) + loadMessages(peerId) + } + + override fun onError(t: Throwable) { + loadConversation(peerId) + loadMessages(peerId) + } + }) + } + + override fun onError(t: Throwable) { + loadConversation(peerId) + loadMessages(peerId) + } + }) + } + + fun loadConversation(peerId: Int) { + if (adapter.isNotEmpty()) { + viewState.hideProgressBar() + } + + repository.loadConversation(peerId, object : MvpOnLoadListener { + + override fun onResponse(response: VKConversation) { + conversation = response + + createAdapter() + refreshConversation(response) + } + + override fun onError(t: Throwable) { + viewState.hideProgressBar() + viewState.showErrorLoadConversationAlert() + } + + }) + } + + private fun refreshConversation(conversation: VKConversation) { + checkIsWritingAllowed(conversation) + + repository.getChatInfo(conversation, object : MvpOnLoadListener { + override fun onResponse(response: String) { + viewState.setChatInfo(response) + } + + override fun onError(t: Throwable) { + viewState.setChatInfo(AppGlobal.resources.getString(R.string.error_obtain_chat_info)) + } + }) + } + + private fun checkIsWritingAllowed(conversation: VKConversation) { + if (conversation.isGroupChannel) { + viewState.hideChatPanel() + return + } + + viewState.showChatPanel() + viewState.setWritingAllowed(conversation.isAllowed) + } + + private fun getCachedMessages( + peerId: Int, + offset: Int = 0, + count: Int = DEFAULT_MESSAGES_COUNT, + listener: MvpOnLoadListener? = null + ) { + repository.getCachedMessages(peerId, offset, count, + object : MvpOnLoadListener> { + override fun onResponse(response: ArrayList) { + viewState.hideProgressBar() + fillAdapter(response, offset) + + listener?.onResponse(null) + } + + override fun onError(t: Throwable) { + if (adapter.isEmpty()) { + viewState.showProgressBar() + } + + listener?.onError(t) + } + }) + } + + private fun loadMessages(peerId: Int, offset: Int = 0, count: Int = DEFAULT_MESSAGES_COUNT) { + repository.loadMessages(peerId, offset, count, + object : MvpOnLoadListener> { + override fun onResponse(response: ArrayList) { + fillAdapter(response, offset) + } + + override fun onError(t: Throwable) { + + } + + }) + } + + private fun fillAdapter( + messages: ArrayList, + offset: Int + ) { + if (adapter.isEmpty()) adapter.isNotCachedValues = true + if (offset == 0) { + adapter.updateValues(messages) + } else { + adapter.addAll(messages) + } + + adapter.notifyDataSetChanged() + + if (offset == 0) recyclerView.scrollToPosition(adapter.itemCount - 1) + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(position: Int) { + + } + + override fun onNewEvent(info: EventInfo<*>) { + + } + + fun sendMessage( + text: String = "", + attachments: ArrayList = arrayListOf(), + scrollToBottom: Boolean = true + ) { + lastMessageText = text + + val message = VKMessage().also { + it.date = (System.currentTimeMillis() / 1000).toInt() + it.text = text + it.isOut = true + it.peerId = peerId + it.fromId = UserConfig.userId + it.randomId = Random.nextInt() + } + + viewState.setMessageText("") + + adapter.addMessage(message, true, scrollToBottom) + + repository.sendMessage(peerId, text, message.randomId, object : MvpOnLoadListener { + override fun onResponse(response: Int) { + message.messageId = response + + TaskManager.execute { MemoryCache.put(message) } + TaskManager.loadMessage(VKApiKeys.UPDATE_MESSAGE, response) + } + + override fun onError(t: Throwable) { + viewState.showErrorSnackbar(t) + + viewState.setMessageText(lastMessageText) + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt new file mode 100644 index 00000000..4e477cef --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/repository/MessagesRepository.kt @@ -0,0 +1,175 @@ +package ru.melod1n.project.vkm.activity.ui.repository + +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKGroup +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpRepository +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.util.ArrayUtils +import java.util.* + +class MessagesRepository : MvpRepository() { + + fun loadMessages( + peerId: Int, + offset: Int, + count: Int, + listener: MvpOnLoadListener> + ) { + TaskManager.execute { + VKApi.messages() + .getHistory() + .peerId(peerId) + .reversed(false) + .extended(true) + .fields(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS) + .offset(offset) + .count(count) + .executeArray( + VKMessage::class.java, + object : OnResponseListener> { + override fun onResponse(response: ArrayList) { + TaskManager.execute { + cacheLoadedMessages(response) + + MemoryCache.putUsers(VKMessage.profiles) + MemoryCache.putGroups(VKMessage.groups) + MemoryCache.putConversations(VKMessage.conversations) + + VKUtil.sortMessagesByDate(response, false) + + sendResponse(listener, response) + } + } + + override fun onError(t: Throwable) { + sendError(listener, t) + } + }) + } + } + + fun getCachedMessages( + peerId: Int, offset: Int, count: Int, + listener: MvpOnLoadListener> + ) { + TaskManager.execute { + val messages = MemoryCache.getMessagesByPeerId(peerId).asArrayList() + + if (messages.isEmpty()) { + sendError(listener, NullPointerException("Messages is empty")) + return@execute + } + + VKUtil.sortMessagesByDate(messages, false) + + val preparedMessages = ArrayUtils.cut(messages, offset, count) + + sendResponseArray(listener, preparedMessages) + } + } + + fun getCachedConversation(peerId: Int, listener: MvpOnLoadListener) { + TaskManager.execute { + val conversation = MemoryCache.getConversationById(peerId) + + if (conversation == null) { + sendError( + listener, + NullPointerException("Conversation is not cached at the moment") + ) + } else { + sendResponse(listener, conversation) + } + } + } + + fun loadConversation(peerId: Int, listener: MvpOnLoadListener) { + TaskManager.loadConversation( + VKApiKeys.UPDATE_CONVERSATION, + peerId, + object : OnResponseListener { + override fun onResponse(response: VKConversation) { + sendResponse(listener, response) + } + + override fun onError(t: Throwable) { + sendError(listener, t) + } + }) + } + + fun getChatInfo(conversation: VKConversation, listener: MvpOnLoadListener) { + when (conversation.type) { + VKConversation.TYPE_CHAT -> { + sendResponse( + listener, + AppGlobal.resources.getString( + if (conversation.isGroupChannel) + R.string.group_channel_members + else R.string.chat_members, + conversation.membersCount + ) + ) + } + VKConversation.TYPE_USER -> { + val user = VKUtil.searchUser(conversation.conversationId, + object : OnResponseListener { + override fun onResponse(response: VKUser) { + sendResponse(listener, VKUtil.getUserOnline(response)) + } + + override fun onError(t: Throwable) { + sendError(listener, t) + } + }) + + user?.let { + sendResponse(listener, VKUtil.getUserOnline(it)) + } + } + else -> { + sendResponse(listener, "") + } + } + } + + fun sendMessage( + peerId: Int, + message: String, + randomId: Int, + listener: MvpOnLoadListener + ) { + TaskManager.execute { + VKApi.messages() + .send() + .peerId(peerId) + .message(message) + .randomId(randomId) + .executeArray(Int::class.java, object : OnResponseListener> { + override fun onResponse(response: ArrayList) { + val messageId = response[0] + sendResponse(listener, messageId) + } + + override fun onError(t: Throwable) { + sendError(listener, t) + } + }) + } + } + + private fun cacheLoadedMessages(messages: ArrayList) { + MemoryCache.putMessages(messages) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt new file mode 100644 index 00000000..2cb95c39 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/activity/ui/view/MessagesView.kt @@ -0,0 +1,24 @@ +package ru.melod1n.project.vkm.activity.ui.view + +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.base.mvp.MvpView + +interface MessagesView : MvpView { + + fun showChatPanel() + + fun hideChatPanel() + + fun setWritingAllowed(allowed: Boolean) + + fun setChatInfo(info: String) + + fun openProfile(conversation: VKConversation) + + fun showErrorLoadConversationAlert() + + fun showVoiceRecordingTip() + + fun setMessageText(text: String) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt new file mode 100644 index 00000000..b4dc05fe --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/ConversationsAdapter.kt @@ -0,0 +1,724 @@ +package ru.melod1n.project.vkm.adapter + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.text.SpannableString +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.adapter.diffutil.ConversationsCallback +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKGroup +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.BaseAdapter +import ru.melod1n.project.vkm.base.BaseHolder +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.widget.CircleImageView + + +@Suppress("UNCHECKED_CAST") +class ConversationsAdapter( + val recyclerView: RecyclerView, + values: ArrayList +) : BaseAdapter( + recyclerView.context, + values +), TaskManager.OnEventListener { + + companion object { + private const val TAG = "ConversationsAdapter" + } + + var isLoading: Boolean = false + private var currentPosition: Int = -1 + + init { + TaskManager.addOnEventListener(this) + } + + override fun destroy() { + TaskManager.removeOnEventListener(this) + } + + override fun onNewEvent(info: EventInfo<*>) { + when (info.key) { + VKApiKeys.NEW_MESSAGE -> addMessage(info.data as VKMessage) + VKApiKeys.EDIT_MESSAGE -> editMessage(info.data as VKMessage) + VKApiKeys.RESTORE_MESSAGE -> restoreMessage(info.data as VKMessage) + VKApiKeys.READ_MESSAGE -> readMessage( + (info.data as Array)[0], + (info.data as Array)[1] + ) + VKApiKeys.DELETE_MESSAGE -> deleteMessage( + (info.data as Array)[0], + (info.data as Array)[1] + ) + + VKApiKeys.UPDATE_CONVERSATION -> updateConversation(info.data as Int) + VKApiKeys.UPDATE_MESSAGE -> updateMessage(info.data as Int) + VKApiKeys.UPDATE_USER -> updateUsers(info.data as ArrayList) + VKApiKeys.UPDATE_GROUP -> updateGroups(info.data as ArrayList) + + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationHolder { + return ConversationHolder(view(R.layout.item_conversation, parent)) + } + + override fun onBindViewHolder( + holder: ConversationHolder, + position: Int, + payloads: MutableList + ) { + currentPosition = position + initListeners(holder.itemView, position) + holder.bind(position, payloads) + } + + fun notifyChanges(oldList: List, newList: List = values) { + val callback = ConversationsCallback(oldList, newList) + val diff = DiffUtil.calculateDiff(callback) + + diff.dispatchUpdatesTo(this) + } + + fun isLastItem() = currentPosition >= itemCount - 1 + + inner class ConversationHolder(v: View) : BaseHolder(v) { + + private var text: TextView = v.findViewById(R.id.conversationText) + private var title: TextView = v.findViewById(R.id.conversationTitle) + private var avatar: ImageView = v.findViewById(R.id.conversationAvatar) + private var online: ImageView = v.findViewById(R.id.conversationUserOnline) + private var out: CircleImageView = v.findViewById(R.id.conversationOut) + private var counter: TextView = v.findViewById(R.id.conversationCounter) + private var date: TextView = v.findViewById(R.id.conversationDate) + private var type: ImageView = v.findViewById(R.id.conversationType) + private var userAvatar: ImageView = v.findViewById(R.id.conversationUserAvatar) + private var root: LinearLayout = v.findViewById(R.id.conversationRoot) + + private val colorHighlight = context.color(R.color.accent) + + override fun bind(position: Int) { + bind(position, mutableListOf()) + } + + fun bind(position: Int, payloads: MutableList) { + Log.d(TAG, "bind position: $position") + + val conversation = this@ConversationsAdapter[position] + + val lastMessage = conversation.lastMessage + + TaskManager.execute { + val peerUser: VKUser? = + if (conversation.isUser()) VKUtil.searchUser(conversation.conversationId) else null + + val peerGroup: VKGroup? = + if (conversation.isGroup()) VKUtil.searchGroup(conversation.conversationId) else null + + val fromUser: VKUser? = + if (lastMessage.isFromUser()) VKUtil.searchUser(lastMessage.fromId) else null + + val fromGroup: VKGroup? = + if (lastMessage.isFromGroup()) VKUtil.searchGroup(lastMessage.fromId) else null + + conversation.peerUser = peerUser + conversation.peerGroup = peerGroup + + lastMessage.fromUser = fromUser + lastMessage.fromGroup = fromGroup + + post { + val dialogTitle = setTitle(conversation, peerUser, peerGroup) + + if (payloads.isNotEmpty()) { + for (payload in payloads) { + when (payload) { + ConversationsCallback.CONVERSATION -> { + setUserOnline(conversation, peerUser) + prepareUserAvatar( + conversation, + lastMessage, + fromUser, + fromGroup + ) + prepareAvatar(dialogTitle, conversation, peerUser, peerGroup) + setDialogType(conversation) + setIsRead(lastMessage, conversation) + setCounterBackground(conversation) + } + ConversationsCallback.MESSAGE -> { + prepareUserAvatar( + conversation, + lastMessage, + fromUser, + fromGroup + ) + prepareAttachments(lastMessage) + setIsRead(lastMessage, conversation) + setDate(lastMessage) + } + ConversationsCallback.GROUP -> { + prepareAvatar(dialogTitle, conversation, peerUser, peerGroup) + } + ConversationsCallback.USER -> { + setUserOnline(conversation, peerUser) + prepareAvatar(dialogTitle, conversation, peerUser, peerGroup) + } + ConversationsCallback.EDIT_MESSAGE -> { + prepareUserAvatar( + conversation, + lastMessage, + fromUser, + fromGroup + ) + prepareAttachments(lastMessage) + setIsRead(lastMessage, conversation) + setDate(lastMessage) + } + ConversationsCallback.DATE -> { + setDate(lastMessage) + } + ConversationsCallback.ONLINE -> { + setUserOnline(conversation, peerUser) + } + ConversationsCallback.ATTACHMENTS -> { + prepareAttachments(lastMessage) + } + ConversationsCallback.AVATAR -> { + prepareAvatar(dialogTitle, conversation, peerUser, peerGroup) + } + ConversationsCallback.USER_AVATAR -> { + prepareUserAvatar( + conversation, + lastMessage, + fromUser, + fromGroup + ) + } + ConversationsCallback.READ -> { + setIsRead(lastMessage, conversation) + } + ConversationsCallback.NOTIFICATIONS -> { + setCounterBackground(conversation) + } + } + } + + return@post + } + + setUserOnline(conversation, peerUser) + + prepareUserAvatar(conversation, lastMessage, fromUser, fromGroup) + + prepareAvatar(dialogTitle, conversation, peerUser, peerGroup) + + setDialogType(conversation) + + prepareAttachments(lastMessage) + + setIsRead(lastMessage, conversation) + + setDate(lastMessage) + + setCounterBackground(conversation) + + root.isVisible = true + } + } + } + + private fun setTitle( + conversation: VKConversation, + peerUser: VKUser?, + peerGroup: VKGroup? + ): String { + val dialogTitle = VKUtil.getTitle(conversation, peerUser, peerGroup) + title.text = dialogTitle + + return dialogTitle + } + + private fun setUserOnline(conversation: VKConversation, peerUser: VKUser?) { + val onlineIcon = VKUtil.getUserOnlineIcon(context, conversation, peerUser) + + online.setImageDrawable(onlineIcon) + online.isVisible = onlineIcon != null + } + + private fun prepareUserAvatar( + conversation: VKConversation, + lastMessage: VKMessage, + fromUser: VKUser?, + fromGroup: VKGroup? + ) { + if ((conversation.isChat() || lastMessage.isOut) && !conversation.isGroupChannel) { + userAvatar.isVisible = true + + val avatar = VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup) + + if (avatar.isEmpty()) { + userAvatar.setImageDrawable(ColorDrawable(Color.TRANSPARENT)) + } else { + userAvatar.setImageURI(Uri.parse(avatar)) + } + +// ImageUtil.loadImage( +// VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup), +// object : ImageUtil.OnLoadListener { +// override fun onLoad(bitmap: Bitmap) { +// userAvatar.setImageBitmap(bitmap) +// } +// +// override fun onError(e: Exception) { +// +// } +// } +// ) + +// ImageUtil.loadImage( +// VKUtil.getUserAvatar(lastMessage, fromUser, fromGroup), +// userAvatar, +// placeholderNormal +// ) + } else { + userAvatar.isVisible = false + userAvatar.setImageDrawable(null) + } + } + + private fun prepareAvatar( + dialogTitle: String, + conversation: VKConversation, + peerUser: VKUser?, + peerGroup: VKGroup? + ) { + val dialogAvatarPlaceholder = VKUtil.getAvatarPlaceholder(context, dialogTitle) + + avatar.setImageDrawable(dialogAvatarPlaceholder) + + val avatarLink = VKUtil.getAvatar(conversation, peerUser, peerGroup) + + if (avatarLink.isNotEmpty()) { + avatar.setImageURI(Uri.parse(avatarLink)) + } + +// ImageUtil.loadImage( +// VKUtil.getAvatar(conversation, peerUser, peerGroup), +// object : ImageUtil.OnLoadListener { +// override fun onLoad(bitmap: Bitmap) { +// avatar.setImageBitmap(bitmap) +// } +// +// override fun onError(e: Exception) { +// +// } +// } +// ) + +// ImageUtil.loadImage( +// VKUtil.getAvatar(conversation, peerUser, peerGroup), +// avatar, +// dialogAvatarPlaceholder +// ) + } + + private fun setDialogType(conversation: VKConversation) { + val dDialogType = VKUtil.getDialogType(context, conversation) + + type.setImageDrawable(dDialogType) + type.isVisible = dDialogType != null + } + + private fun prepareAttachments(lastMessage: VKMessage) { + text.apply { + compoundDrawablePadding = 0 + setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null) + } + + if (lastMessage.action == null) { + when { + lastMessage.attachments.isNotEmpty() -> { + val attachmentString = + VKUtil.getAttachmentText(context, lastMessage.attachments) + + val attachmentText = + if (lastMessage.text.isEmpty()) attachmentString else lastMessage.text + + val startIndex = + if (lastMessage.text.isEmpty()) 0 else lastMessage.text.length + + val span = SpannableString(attachmentText).apply { + setSpan( + ForegroundColorSpan(colorHighlight), + startIndex, + attachmentText.length, + 0 + ) + } + + val attachmentDrawable = + VKUtil.getAttachmentDrawable(context, lastMessage.attachments) + + text.apply { + text = span + setCompoundDrawablesRelativeWithIntrinsicBounds( + attachmentDrawable, + null, + null, + null + ) + compoundDrawablePadding = 8 + } + } + lastMessage.fwdMessages.isNotEmpty() -> { + val fwdText = VKUtil.getFwdText(context, lastMessage.getForwardedMessages()) + val span = SpannableString(fwdText).apply { + setSpan(ForegroundColorSpan(colorHighlight), 0, fwdText.length, 0) + } + + text.text = span + } + else -> { + text.text = if (text.maxLines == 1) lastMessage.text.replace( + "\n", + " " + ) else lastMessage.text + } + } + } else { + VKUtil.getActionText(context, lastMessage, + object : OnResponseListener { + override fun onResponse(response: String) { + val span = SpannableString(response).apply { + setSpan( + ForegroundColorSpan(colorHighlight), + 0, + response.length, + 0 + ) + } + + text.text = span + } + + override fun onError(t: Throwable) { + TODO("Not yet implemented") + } + + }) + + } + + if (lastMessage.attachments.isEmpty() && lastMessage.fwdMessages.isEmpty() && lastMessage.action == null && TextUtils.isEmpty( + lastMessage.text + ) + ) { + val unknown = "..." + val span = SpannableString(unknown).apply { + setSpan(ForegroundColorSpan(colorHighlight), 0, unknown.length, 0) + } + + text.text = span + } + } + + private fun setIsRead(lastMessage: VKMessage, conversation: VKConversation) { + val isRead = + ((lastMessage.isOut && conversation.outRead == conversation.lastMessageId || + !lastMessage.isOut && conversation.inRead == conversation.lastMessageId) && conversation.lastMessageId == lastMessage.messageId) && conversation.unreadCount == 0 + + if (isRead) { + counter.visibility = View.GONE + out.visibility = View.GONE + } else { + if (lastMessage.isOut) { + out.visibility = View.VISIBLE + counter.visibility = View.GONE + counter.text = "" + } else { + out.visibility = View.GONE + counter.visibility = View.VISIBLE + counter.text = conversation.unreadCount.toString() + } + } + } + + private fun setDate(lastMessage: VKMessage) { + val dateText = VKUtil.getTime(context, lastMessage) + date.text = dateText + } + + private fun setCounterBackground(conversation: VKConversation) { + counter.background.setTint(if (conversation.isNotificationsDisabled()) Color.GRAY else colorHighlight) + } + } + + @Deprecated("Message is bad") + private fun addMessage(message: VKMessage) { + val index = searchConversationIndex(message.peerId) + + val oldList = ArrayList(values) + + if (index >= 0) { + val currentConversation = this[index] + + val conversation = prepareConversation(currentConversation, message) + + removeAt(index) + add(0, conversation) + notifyChanges(oldList) + } else { + TaskManager.loadConversation( + VKApiKeys.UPDATE_CONVERSATION, + message.peerId, + null + ) + + TaskManager.execute { + val cachedConversation = MemoryCache.getConversationById(message.peerId) + if (cachedConversation != null) { + add(0, prepareConversation(cachedConversation, message)) + post { notifyChanges(oldList) } + return@execute + } + + val tempConversations = VKConversation().apply { + conversationId = message.peerId + + localId = + if (VKUtil.isChatId(conversationId)) conversationId - 2000000000 else conversationId + type = + if (conversationId < 0) VKConversation.TYPE_GROUP else if (conversationId > 2000000000) VKConversation.TYPE_CHAT else VKConversation.TYPE_USER + + lastMessage = message + lastMessageId = message.messageId + } + + add(0, tempConversations) + + post { notifyChanges(oldList) } + } + } + + val firstVisiblePosition = + (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + + if (firstVisiblePosition <= 1) recyclerView.scrollToPosition(0) + } + + private fun editMessage(message: VKMessage) { + val index = searchConversationIndex(message.peerId) + if (index == -1) return + + val conversation = getItem(index) + + if (conversation.lastMessageId != message.messageId) return + + conversation.lastMessage = message + + notifyItemChanged(index, ConversationsCallback.EDIT_MESSAGE) + } + + private fun readMessage(peerId: Int, messageId: Int) { + val index = searchConversationIndex(peerId) + if (index == -1) return + + val conversation = getItem(index) + val message = conversation.lastMessage + + if (message.isInbox()) { + conversation.inRead = messageId + } else { + conversation.outRead = messageId + } + + conversation.unreadCount = if (conversation.lastMessageId == messageId) { + 0 + } else { + conversation.lastMessageId - messageId + } + + notifyItemChanged(index, ConversationsCallback.READ) + } + + @Deprecated("Need to rewrite") + private fun deleteMessage(peerId: Int, messageId: Int) { + return + val index = searchConversationIndex(peerId) + if (index == -1) return + + val oldList = ArrayList(values) + + val oldDialog = values[index] + + val dialog = oldDialog.clone() + + TaskManager.execute { + val cachedMessages = MemoryCache.getMessagesByPeerId(dialog.conversationId) + val messages = VKUtil.sortMessagesByDate(ArrayList(cachedMessages), true) + + if (messages.isEmpty()) { + MemoryCache.deleteConversation(dialog.conversationId) + + AppGlobal.post { + removeAt(index) + notifyChanges(oldList) + } + } else { + val lastMessage = messages[0] + + dialog.lastMessageId = lastMessage.messageId + dialog.lastMessage = lastMessage + + set(index, dialog) + + VKUtil.sortConversationsByDate(values, true) + + AppGlobal.post { + notifyChanges(oldList) + } + } + } + } + + @Deprecated("Message is bad") + private fun restoreMessage(message: VKMessage) { + val index = searchConversationIndex(message.peerId) + if (index == -1) return + + val oldList = ArrayList().apply { addAll(values) } + val oldDialog = values[index] + + val dialog = oldDialog.clone() + + TaskManager.execute { + val messages = + MemoryCache.getMessagesByPeerId(dialog.conversationId).apply { addMessage(message) } + + VKUtil.sortMessagesByDate(ArrayList(messages), true) + + val lastMessage = messages[0] + + dialog.lastMessageId = lastMessage.messageId + dialog.lastMessage = lastMessage + + set(index, dialog) + + VKUtil.sortConversationsByDate(values, true) + + AppGlobal.handler.post { + notifyChanges(oldList) + +// fragmentConversations.presenter.checkListIsEmpty(values) + } + } + } + + private fun prepareConversation( + conversation: VKConversation, + newMessage: VKMessage + ): VKConversation { + conversation.lastMessage = newMessage + conversation.lastMessageId = newMessage.messageId + + if (newMessage.isOut) { + conversation.unreadCount = 0 + newMessage.isRead = false + } else { + conversation.unreadCount++ + } + + if (newMessage.peerId == newMessage.fromId && newMessage.fromId == UserConfig.userId) { //для лс + conversation.outRead = newMessage.messageId + } + + return conversation + } + + private fun searchConversationIndex(peerId: Int): Int { + for (i in values.indices) { + if (getItem(i).conversationId == peerId) return i + } + return -1 + } + + private fun searchMessageIndex(messageId: Int): Int { + for (i in values.indices) { + if (getItem(i).lastMessageId == messageId) return i + } + return -1 + } + + private fun updateConversation(peerId: Int) { + val index = searchConversationIndex(peerId) + if (index == -1) return + + TaskManager.execute { + val conversation = MemoryCache.getConversationById(peerId) ?: return@execute + + set(index, conversation) + + AppGlobal.post { notifyItemChanged(index, ConversationsCallback.CONVERSATION) } + } + } + + private fun updateGroups(groupIds: ArrayList) { + for (groupId in groupIds) { + val index = searchConversationIndex(groupId) + if (index == -1) return + + notifyItemChanged(index) + } + } + + private fun updateUsers(userIds: ArrayList) { + for (userId in userIds) { + val index = searchConversationIndex(userId) + if (index == -1) return + + notifyItemChanged(index) + } + } + + private fun updateMessage(messageId: Int) { + val index = searchMessageIndex(messageId) + if (index == -1) return + + + TaskManager.execute { + val conversation = getItem(index).clone() + + conversation.apply { + lastMessageId = messageId + lastMessage = MemoryCache.getMessageById(messageId) ?: return@execute + } + + AppGlobal.handler.post { notifyItemChanged(index, ConversationsCallback.MESSAGE) } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt new file mode 100644 index 00000000..f7d3dd1b --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/MessagesAdapter.kt @@ -0,0 +1,581 @@ +package ru.melod1n.project.vkm.adapter + +import android.content.Context +import android.graphics.Typeface +import android.text.SpannableString +import android.text.style.StyleSpan +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.MessagesActivity +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.* +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.BaseAdapter +import ru.melod1n.project.vkm.base.BaseHolder +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.extensions.FloatExtensions.int +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.util.AndroidUtils +import ru.melod1n.project.vkm.util.ImageUtils +import ru.melod1n.project.vkm.widget.BoundedLinearLayout +import ru.melod1n.project.vkm.widget.CircleImageView +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.abs + + +@Suppress("UNCHECKED_CAST") +class MessagesAdapter( + context: Context, + values: ArrayList, + var conversation: VKConversation + +) : BaseAdapter(context, values), + TaskManager.OnEventListener { + + companion object { + private const val TYPE_FOOTER = 10101 + + private const val TYPE_NORMAL_IN = 7910 + private const val TYPE_NORMAL_OUT = 7911 + + private const val TYPE_ATTACHMENT_IN = 7920 + private const val TYPE_ATTACHMENT_OUT = 7921 + + private const val TYPE_ACTION = 7930 + + private const val TYPE_NORMAL_CHANNEL = 7940 + + const val TAG = "MessagesAdapter" + } + + private var recyclerView = (context as MessagesActivity).recyclerView + private var layoutManager = recyclerView.layoutManager as LinearLayoutManager + + var isNotCachedValues = false + + init { + TaskManager.addOnEventListener(this) + } + + override fun destroy() { + TaskManager.removeOnEventListener(this) + } + + override fun onNewEvent(info: EventInfo<*>) { + when (info.key) { + VKApiKeys.NEW_MESSAGE -> addMessage(info.data as VKMessage) + + VKApiKeys.READ_MESSAGE -> readMessage( + (info.data as Array)[0], + (info.data as Array)[1] + ) + + VKApiKeys.RESTORE_MESSAGE -> restoreMessage(info.data as VKMessage) + VKApiKeys.EDIT_MESSAGE -> editMessage(info.data as VKMessage) + VKApiKeys.DELETE_MESSAGE -> deleteMessage( + (info.data as Array)[0], + (info.data as Array)[1] + ) + + VKApiKeys.UPDATE_MESSAGE -> updateMessage(info.data as Int) + VKApiKeys.UPDATE_USER -> updateUser(info.data as ArrayList) + VKApiKeys.UPDATE_GROUP -> updateGroup(info.data as ArrayList) + + else -> return + } + } + + override fun getItemCount(): Int { + return values.size + 1 + } + + override fun getItemViewType(position: Int): Int { + if (position == values.size) return TYPE_FOOTER + + val message = getItem(position) + + return when { + message.action != null -> TYPE_ACTION + conversation.isGroupChannel -> TYPE_NORMAL_CHANNEL + message.isOut && message.attachments.isEmpty() && message.fwdMessages.isEmpty() -> TYPE_NORMAL_OUT + !message.isOut && message.attachments.isEmpty() && message.fwdMessages.isEmpty() -> TYPE_NORMAL_IN + message.isOut && (message.attachments.isNotEmpty() || message.fwdMessages.isNotEmpty()) -> TYPE_ATTACHMENT_OUT + !message.isOut && (message.attachments.isNotEmpty() || message.fwdMessages.isNotEmpty()) -> TYPE_ATTACHMENT_IN + else -> 0 + } + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, type: Int): Holder { + return when (type) { + TYPE_FOOTER -> FooterHolder(generateEmptyView()) + + TYPE_NORMAL_IN -> ItemNormalIn(view(R.layout.item_message_normal_in, viewGroup)) + TYPE_NORMAL_OUT -> ItemNormalOut(view(R.layout.item_message_normal_out, viewGroup)) + + TYPE_ATTACHMENT_IN -> ItemAttachmentIn( + view( + R.layout.item_message_attachment_in, + viewGroup + ) + ) + TYPE_ATTACHMENT_OUT -> ItemAttachmentOut( + view( + R.layout.item_message_attachment_out, + viewGroup + ) + ) + + TYPE_ACTION -> ItemAction(view(R.layout.item_message_action, viewGroup)) + + TYPE_NORMAL_CHANNEL -> ItemChannel(view(R.layout.item_message_channel, viewGroup)) + + else -> PlaceHolder(view(R.layout.item_message, viewGroup)) + } + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "bind position: $position") + } + + if (holder is FooterHolder) return + + super.onBindViewHolder(holder, position) + if (!isNotCachedValues) return + + val message = this[position] + + if (message.isUnreaded()) { + TaskManager.readMessage( + VKApiKeys.READ_MESSAGE, + conversation.conversationId, + message.messageId + ) + } + } + + private fun generateEmptyView(): View { + return View(context).also { + it.isFocusable = false + it.isClickable = false + it.isEnabled = false + it.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + if (conversation.isGroupChannel) 0 else AndroidUtils.px(74f).int() + ) + } + } + + inner class FooterHolder(v: View) : Holder(v) { + override fun bind(position: Int) {} + } + + inner class ItemChannel(v: View) : ItemNormalIn(v) { + private val title: TextView = v.findViewById(R.id.channelTitle) + + override fun bind(position: Int) { + val message = getItem(position) + + ViewController().prepareDate(message, date) + + val avatarString = conversation.photo100 + + val placeHolder = VKUtil.getAvatarPlaceholder(context, conversation.title) + + avatar.setImageDrawable(placeHolder) + ImageUtils.loadImage(avatarString, avatar, placeHolder) + + title.text = conversation.title + + text.text = message.text + + root.visibility = View.VISIBLE + } + } + + inner class ItemAction(v: View) : Holder(v) { + private val text: TextView = v.findViewById(R.id.messageAction) + + override fun bind(position: Int) { + val message = getItem(position) + + TaskManager.execute { + val user = searchUser(message) + val group = searchGroup(message) + + val name = + (if (group == null && !VKGroup.isGroupId(message.fromId)) user?.firstName else group?.name) + ?: "null" + + VKUtil.getActionText(context, message, object : OnResponseListener { + + override fun onResponse(response: String) { + val actionText = "$name $response" + + val spannable = SpannableString(actionText) + spannable.setSpan(StyleSpan(Typeface.BOLD), 0, name.length, 0) + + text.text = spannable + } + + override fun onError(t: Throwable) { + } + }) + + post { text.isVisible = true } + } + } + } + + open inner class ItemNormalIn(v: View) : NormalViewHolder(v) { + + override fun bind(position: Int) { + val message = getItem(position) + + TaskManager.execute { + val user = searchUser(message) + val group = searchGroup(message) + + post { + ViewController().apply { + prepareText(message, bubble, text) + prepareDate(message, date) + prepareAvatar(message, avatar) + loadAvatarImage(message, user, group, avatar) + } + + root.isVisible = true + } + } + } + } + + inner class ItemAttachmentIn(v: View) : ItemNormalIn(v) { + val attachments: LinearLayout = v.findViewById(R.id.messageAttachments) + + override fun bind(position: Int) { + super.bind(position) + + val message = getItem(position) + + AttachmentInflater.showAttachments(message, this) + } + } + + open inner class ItemNormalOut(v: View) : NormalViewHolder(v) { + + override fun bind(position: Int) { + val message = getItem(position) + + TaskManager.execute { + val user = searchUser(message) + val group = searchGroup(message) + + post { + ViewController().apply { + prepareText(message, bubble, text) + prepareDate(message, date) + prepareAvatar(message, avatar) + loadAvatarImage(message, user, group, avatar) + } + + root.isVisible = true + } + } + } + } + + inner class ItemAttachmentOut(v: View) : ItemNormalOut(v) { + + val attachments: LinearLayout = v.findViewById(R.id.messageAttachments) + + override fun bind(position: Int) { + super.bind(position) + + val message = getItem(position) + + AttachmentInflater.showAttachments(message, this) + } + + } + + abstract inner class NormalViewHolder(v: View) : Holder(v) { + protected val date: TextView = v.findViewById(R.id.messageDate) + protected val text: TextView = v.findViewById(R.id.messageText) + protected val root: LinearLayout = v.findViewById(R.id.messageRoot) + protected val bubble: BoundedLinearLayout = v.findViewById(R.id.messageBubble) + protected val avatar: CircleImageView = v.findViewById(R.id.messageAvatar) + } + + object AttachmentInflater { + fun showAttachments(message: VKMessage, holder: NormalViewHolder) { + val attachments = + (if (holder is ItemAttachmentOut) holder.attachments else if (holder is ItemAttachmentIn) holder.attachments else null) + ?: return + + if (message.fwdMessages.isNotEmpty() || message.attachments.isNotEmpty()) { + attachments.visibility = View.VISIBLE + attachments.removeAllViews() + } else { + attachments.visibility = View.GONE + } + + if (message.attachments.isNotEmpty()) { + prepareAttachments(message, attachments) + } + + if (message.fwdMessages.isNotEmpty()) { + prepareForwardedMessages(message, attachments) + } + } + + private fun prepareAttachments(message: VKMessage, attachments: LinearLayout) { + for (attachment in message.attachments) { + when (attachment) { + is VKPhoto -> photo(message, attachments) + is VKVideo -> video(message, attachments) + is VKLink -> link(message, attachments) + is VKAudio -> audio(message, attachments) + is VKDoc -> doc(message, attachments) + } + } + } + + private fun prepareForwardedMessages(message: VKMessage, attachments: LinearLayout) { + + } + + fun link(message: VKMessage, attachments: LinearLayout) { + + } + + fun video(message: VKMessage, attachments: LinearLayout) { + + } + + fun photo(message: VKMessage, attachments: LinearLayout) { + } + + fun audio(message: VKMessage, attachments: LinearLayout) { + + } + + fun doc(message: VKMessage, attachments: LinearLayout) { + + } + } + + inner class ViewController { + + fun prepareText(message: VKMessage, bubble: BoundedLinearLayout, text: TextView) { + val screenWidth = context.resources.displayMetrics.widthPixels + val boundedWidth = screenWidth - screenWidth / 5 + bubble.maxWidth = boundedWidth + + text.text = VKUtil.matchMentions(message.text) + } + + fun prepareDate(message: VKMessage, date: TextView) { + var dateText = + SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L) + + if (message.editTime > 0) { + dateText += ", ${ + context.getString(R.string.edited) + .toLowerCase(Locale.getDefault()) + }" + } + + date.text = dateText + } + + fun prepareAvatar(message: VKMessage, avatar: ImageView) { + avatar.isVisible = !message.isOut + } + + fun loadAvatarImage(message: VKMessage, user: VKUser?, group: VKGroup?, avatar: ImageView) { + val dialogTitle = VKUtil.getMessageTitle(message, user, group) + val avatarPlaceholder = VKUtil.getAvatarPlaceholder(context, dialogTitle) + + avatar.setImageDrawable(avatarPlaceholder) + + val avatarString = VKUtil.getUserAvatar(message, user, group) + + ImageUtils.loadImage(avatarString, avatar, avatarPlaceholder) + } + + } + + open inner class Holder(v: View) : BaseHolder(v) { + override fun bind(position: Int) { + } + } + + inner class PlaceHolder(v: View) : NormalViewHolder(v) + + private fun searchUser(message: VKMessage): VKUser? { + if (!message.isFromUser()) return null + + return VKUtil.searchUser(message.fromId) + } + + private fun searchGroup(message: VKMessage): VKGroup? { + if (!message.isFromGroup()) return null + + return VKUtil.searchGroup(message.fromId) + } + + private fun updateGroup(groupIds: ArrayList) { + for (groupId in groupIds) { + var index = -1 + + for (i in values.indices) { + val item = getItem(i) + + if (abs(item.fromId) == groupId) { + index = i + break + } + } + + if (index == -1) return + + notifyItemChanged(index) + } + } + + private fun updateUser(userIds: ArrayList) { + for (userId in userIds) { + var index = -1 + + for (i in values.indices) { + val item = getItem(i) + + if (item.fromId == userId) { + index = i + break + } + } + + if (index == -1) return + notifyItemChanged(index) + } + } + + private fun updateMessage(messageId: Int) { + var index = -1 + + for (i in values.indices) { + val item = getItem(i) + + if (item.messageId == messageId) { + index = i + break + } + } + + if (index == -1) return + + TaskManager.execute { + AppGlobal.database.messages.getById(messageId)?.let { + values[index] = it + + post { notifyItemChanged(index) } + } + } + } + + private fun searchMessagePosition(messageId: Int): Int { + for (i in values.indices) { + if (getItem(i).messageId == messageId) return i + } + + return -1 + } + + private fun containsRandomId(randomId: Int): Boolean { + for (message in values) { + if (message.randomId == randomId) return true + } + + return false + } + + fun addMessage(message: VKMessage, fromApp: Boolean = false, withScroll: Boolean = false) { + val randomId = message.randomId + if (randomId > 0 && containsRandomId(message.randomId) || message.peerId != conversation.conversationId) return + + add(message) + + notifyDataSetChanged() + + val lastVisiblePosition = layoutManager.findLastVisibleItemPosition() + + if ((message.isInbox() && lastVisiblePosition >= itemCount - 2) || !fromApp || withScroll) { + recyclerView.scrollToPosition(itemCount - 1) + } + } + + private fun readMessage(peerId: Int, messageId: Int) { + if (peerId != conversation.conversationId) return + + val index = searchMessagePosition(messageId) + if (index == -1) return + + val message = this[index] + message.isRead = true + + notifyDataSetChanged() + + if (message.isInbox()) { + conversation.inRead = messageId + } else { + conversation.outRead = messageId + } + + conversation.unreadCount-- + + TaskManager.execute { + MemoryCache.put(message) + MemoryCache.put(conversation) + } + } + + fun editMessage(message: VKMessage) { + val index = searchMessagePosition(message.messageId) + if (index == -1) return + + set(index, message) + notifyDataSetChanged() + } + + fun deleteMessage(messageId: Int, peerId: Int) { + if (peerId != conversation.conversationId) return + + val index = searchMessagePosition(messageId) + if (index == -1) return + + removeAt(index) + notifyDataSetChanged() + } + + //TODO: кривое сообщение + fun restoreMessage(message: VKMessage) { + if (message.peerId != conversation.conversationId) return + + updateValues(VKUtil.sortMessagesByDate(values.apply { add(message) }, false)) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt new file mode 100644 index 00000000..2ebdc81d --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/SimpleItemAdapter.kt @@ -0,0 +1,35 @@ +package ru.melod1n.project.vkm.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseAdapter +import ru.melod1n.project.vkm.base.BaseHolder +import ru.melod1n.project.vkm.item.SimpleMenuItem +import java.util.* + +class SimpleItemAdapter(context: Context, values: ArrayList) : + BaseAdapter(context, values) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(view(R.layout.item_simple_menu, parent)) + } + + inner class ViewHolder(v: View) : BaseHolder(v) { + + private val title: TextView = v.findViewById(R.id.profileItemTitle) + private val icon: ImageView = v.findViewById(R.id.profileItemIcon) + + override fun bind(position: Int) { + val item = getItem(position) + + title.text = item.title + + icon.setImageDrawable(item.icon) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt new file mode 100644 index 00000000..559cebce --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/UsersAdapter.kt @@ -0,0 +1,68 @@ +package ru.melod1n.project.vkm.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.adapter.diffutil.UsersCallback +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.BaseAdapter +import ru.melod1n.project.vkm.base.BaseHolder +import ru.melod1n.project.vkm.util.ImageUtils +import ru.melod1n.project.vkm.widget.CircleImageView + +class UsersAdapter(context: Context, values: ArrayList) : + BaseAdapter(context, values) { + + var isLoading: Boolean = false + var currentPosition: Int = 0 + + fun isLastItem() = currentPosition >= itemCount - 1 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(view(R.layout.item_user, parent)) + } + + open inner class ViewHolder(v: View) : BaseHolder(v) { + private val avatar: CircleImageView = v.findViewById(R.id.userAvatar) + private val name: TextView = v.findViewById(R.id.userName) + private val online: ImageView = v.findViewById(R.id.userOnline) + private val onlineText: TextView = v.findViewById(R.id.userOnlineText) + + override fun bind(position: Int) { + currentPosition = position + + val user = getItem(position) + + name.text = user.toString() + + val avatarPlaceholder = VKUtil.getAvatarPlaceholder(context, user.toString()) + avatar.setImageDrawable(avatarPlaceholder) + + ImageUtils.loadImage(user.photo200, avatar, avatarPlaceholder) + + online.isVisible = false + + VKUtil.getUserOnlineIcon(context, user)?.let { + online.setImageDrawable(it) + online.isVisible = true + } + + onlineText.text = VKUtil.getUserOnline(user) + + //TODO: отладить открытие чата + } + } + + fun notifyChanges(oldList: List, newList: List = values) { + val callback = UsersCallback(oldList, newList) + val diff = DiffUtil.calculateDiff(callback, false) + + diff.dispatchUpdatesTo(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt new file mode 100644 index 00000000..651f0df9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/ConversationsCallback.kt @@ -0,0 +1,95 @@ +package ru.melod1n.project.vkm.adapter.diffutil + +import androidx.recyclerview.widget.DiffUtil +import ru.melod1n.project.vkm.api.model.VKConversation + +class ConversationsCallback( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + + companion object { + const val DATE = "date" + const val ONLINE = "online" + const val AVATAR = "avatar" + const val USER_AVATAR = "user_avatar" + const val ATTACHMENTS = "attachments" + const val READ = "read" + const val NOTIFICATIONS = "notifications" + const val EDIT_MESSAGE = "edit_message" + const val MESSAGE = "message" + const val USER = "user" + const val GROUP = "group" + const val CONVERSATION = "conversation" + } + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + + if (true) return false + return old.conversationId == new.conversationId + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + + val oldMessage = old.lastMessage + val newMessage = new.lastMessage + + if (true) return false else + + return old.title == new.title && + old.lastMessageId == new.lastMessageId && + old.photo50 == new.photo50 && + old.unreadCount == new.unreadCount && + + old.isNoSound == new.isNoSound && + old.isDisabledForever == new.isDisabledForever && + old.disabledUntil == new.disabledUntil && + + old.inRead == new.inRead && + old.outRead == new.outRead && + + old.peerUser == new.peerUser && + old.peerGroup == new.peerGroup && + + oldMessage == newMessage + +// oldMessage.messageId == newMessage.messageId && +// oldMessage.isOut == newMessage.isOut && +// oldMessage.fromId == newMessage.fromId && +// oldMessage.date == newMessage.date && +// oldMessage.action == newMessage.action && +// oldMessage.text == newMessage.text && +// oldMessage.attachments == newMessage.attachments && +// oldMessage.fwdMessages == newMessage.fwdMessages && +// oldMessage.messageId == newMessage.messageId && +// +// oldMessage.fromUser == newMessage.fromUser && +// oldMessage.fromGroup == newMessage.fromGroup + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldConversation = oldList[oldItemPosition] + val newConversation = newList[newItemPosition] + + val oldMessage = oldConversation.lastMessage + val newMessage = newConversation.lastMessage + + val oldDate = oldMessage.date + val newDate = newMessage.date + +// if (oldDate != newDate) return DATE + +// if (oldMessage != newMessage) return MESSAGE + + return super.getChangePayload(oldItemPosition, newItemPosition) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt new file mode 100644 index 00000000..9c18fcf9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/adapter/diffutil/UsersCallback.kt @@ -0,0 +1,50 @@ +package ru.melod1n.project.vkm.adapter.diffutil + +import androidx.recyclerview.widget.DiffUtil +import ru.melod1n.project.vkm.api.model.VKUser + +class UsersCallback(private val oldList: List, private val newList: List) : DiffUtil.Callback() { + + companion object { + const val ONLINE = "online" + const val ONLINE_MOBILE = "online_mobile" + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + + return old.userId == new.userId + } + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + + return old.firstName == new.firstName && + old.lastName == new.lastName && + old.isOnline == new.isOnline && + old.isOnlineMobile == new.isOnlineMobile && + old.lastSeen == new.lastSeen && + old.lastSeenPlatform == new.lastSeenPlatform && + old.deactivated == new.deactivated + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val old = oldList[oldItemPosition] + val new = newList[newItemPosition] + + if (old.isOnlineMobile != new.isOnlineMobile) { + if (old.isOnline != new.isOnline) return ONLINE + + return ONLINE_MOBILE + } + + return super.getChangePayload(oldItemPosition, newItemPosition) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt b/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt new file mode 100644 index 00000000..2b516b6f --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/ErrorCodes.kt @@ -0,0 +1,42 @@ +package ru.melod1n.project.vkm.api + +object ErrorCodes { + const val UNKNOWN_ERROR = 1 + const val APP_DISABLED = 2 + const val UNKNOWN_METHOD = 3 + const val INVALID_SIGNATURE = 4 + const val USER_AUTHORIZATION_FAILED = 5 + const val TOO_MANY_REQUESTS = 6 + const val NO_RIGHTS = 7 + const val BAD_REQUEST = 8 + const val TOO_MANY_SIMILAR_ACTIONS = 9 + const val INTERNAL_SERVER_ERROR = 10 + const val IN_TEST_MODE = 11 + const val EXECUTE_CODE_COMPILE_ERROR = 12 + const val EXECUTE_CODE_RUNTIME_ERROR = 13 + const val CAPTCHA_NEEDED = 14 + const val ACCESS_DENIED = 15 + const val REQUIRES_REQUESTS_OVER_HTTPS = 16 + const val VALIDATION_REQUIRED = 17 + const val USER_BANNED_OR_DELETED = 18 + const val ACTION_PROHIBITED = 20 + const val ACTION_ALLOWED_ONLY_FOR_STANDALONE = 21 + const val METHOD_OFF = 23 + const val CONFIRMATION_REQUIRED = 24 + const val PARAMETER_IS_NOT_SPECIFIED = 100 + const val INCORRECT_APP_ID = 101 + const val OUT_OF_LIMITS = 103 + const val INCORRECT_USER_ID = 113 + const val INCORRECT_TIMESTAMP = 150 + const val ACCESS_TO_ALBUM_DENIED = 200 + const val ACCESS_TO_AUDIO_DENIED = 201 + const val ACCESS_TO_GROUP_DENIED = 203 + const val ALBUM_IS_FULL = 300 + const val ACTION_DENIED = 500 + const val PERMISSION_DENIED = 600 + const val CANNOT_SEND_MESSAGE_BLACK_LIST = 900 + const val CANNOT_SEND_MESSAGE_GROUP = 901 + const val INVALID_DOC_ID = 1150 + const val INVALID_DOC_TITLE = 1152 + const val ACCESS_TO_DOC_DENIED = 1153 +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt b/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt new file mode 100644 index 00000000..3738a6e9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/UserConfig.kt @@ -0,0 +1,41 @@ +package ru.melod1n.project.vkm.api + +import android.text.TextUtils +import ru.melod1n.project.vkm.common.AppGlobal + +object UserConfig { + + private const val TOKEN = "token" + private const val USER_ID = "user_id" + + const val API_ID = "6964679" + + var token = "" + var userId = 0 + + fun save() { + AppGlobal.preferences.edit() + .putString(TOKEN, token) + .putInt(USER_ID, userId) + .apply() + } + + fun restore() { + token = AppGlobal.preferences.getString(TOKEN, "")!! + userId = AppGlobal.preferences.getInt(USER_ID, -1) + } + + fun clear() { + token = "" + userId = -1 + + AppGlobal.preferences.edit() + .remove(TOKEN) + .remove(USER_ID) + .apply() + } + + fun isLoggedIn(): Boolean { + return userId > 0 && !TextUtils.isEmpty(token) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt new file mode 100644 index 00000000..4d0a0018 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKApi.kt @@ -0,0 +1,504 @@ +package ru.melod1n.project.vkm.api + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.annotation.WorkerThread +import org.json.JSONArray +import org.json.JSONObject +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.activity.DropUserDataActivity +import ru.melod1n.project.vkm.api.method.MessageMethodSetter +import ru.melod1n.project.vkm.api.method.MethodSetter +import ru.melod1n.project.vkm.api.method.UserMethodSetter +import ru.melod1n.project.vkm.api.model.* +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.net.HttpRequest +import java.util.* +import kotlin.collections.ArrayList + +@Suppress("UNCHECKED_CAST") +object VKApi { + + private var context: Context? = null + + private const val TAG = "VKM:VKApi" + + const val BASE_URL = "https://api.vk.com/method/" + + const val API_VERSION = "5.132" + + val language: String = AppGlobal.locale.language + + fun init(context: Context) { + this.context = context + } + + @WorkerThread + @Suppress("UNCHECKED_CAST") + fun execute(url: String, cls: Class?): ArrayList? { + if (BuildConfig.DEBUG) { + Log.w(TAG, "url: $url") + } + + val buffer = HttpRequest[url].asString() + if (BuildConfig.DEBUG) { + Log.i(TAG, "response: $buffer") + } + + val json = JSONObject(buffer) + + try { + checkError(json, url) + } catch (ex: VKException) { + if (ex.code == ErrorCodes.TOO_MANY_REQUESTS) { + Timer().schedule(object : TimerTask() { + override fun run() { + execute(url, cls) + } + }, 1000) + } else throw ex + } + + when (cls) { + null -> return null + + VKLongPollServer::class.java -> { + json.optJSONObject("response")?.let { + return arrayListOf(VKLongPollServer(it)) as ArrayList? + } + } + + Boolean::class.java -> { + val value = json.optInt("response") == 1 + return arrayListOf(value) as ArrayList? + } + + Long::class.java -> { + val value = json.optLong("response") + return arrayListOf(value) as ArrayList? + } + + Int::class.java -> { + val value = json.optInt("response") + return arrayListOf(value) as ArrayList? + } + } + + val response = json.opt("response") ?: return null + + val array = optItems(json) ?: return null + val models = ArrayList(array.length()) + + when (cls) { + VKUser::class.java -> { + json.optJSONObject("response")?.let { r -> + VKUser.friendsCount = r.optInt("count") + } + + for (i in 0 until array.length()) { + models.add(VKUser(array.optJSONObject(i)) as T) + } + } + + VKMessage::class.java -> { + response as JSONObject + + if (url.contains("messages.getHistory")) { + VKMessage.lastHistoryCount = response.optInt("count") + + response.optJSONArray("profiles")?.let { + val profiles = arrayListOf() + + for (j in 0 until it.length()) { + profiles.add(VKUser(it.optJSONObject(j))) + } + + VKMessage.profiles = profiles + } + + response.optJSONArray("groups")?.let { + val groups = arrayListOf() + + for (j in 0 until it.length()) { + groups.add(VKGroup(it.optJSONObject(j))) + } + + VKMessage.groups = groups + } + + response.optJSONArray("conversations")?.let { + val conversations = arrayListOf() + + for (j in 0 until it.length()) { + conversations.add(VKConversation(it.optJSONObject(j))) + } + + VKMessage.conversations = conversations + } + } + + for (i in 0 until array.length()) { + var source = array.optJSONObject(i) + if (source.has("message")) { + source = source.optJSONObject("message") + } + + val message = VKMessage(source) + models.add(message as T) + } + + val profiles = ArrayList() + response.optJSONArray("profiles")?.let { + profiles.addAll(VKUser.parse(it)) + } + + val groups = ArrayList() + response.optJSONArray("groups")?.let { + groups.addAll(VKGroup.parse(it)) + } + + AppGlobal.database.let { + it.users.insert(profiles) + it.groups.insert(groups) + } + } + + VKGroup::class.java -> { + for (i in 0 until array.length()) { + models.add(VKGroup(array.optJSONObject(i)) as T) + } + } + + VKModel::class.java -> { + if (url.contains("messages.getHistoryAttachments")) { + return VKAttachments.parse(array) as ArrayList + } + } + + VKConversation::class.java -> { + if (url.contains("getConversationsById")) { + for (i in 0 until array.length()) { + val source = array.optJSONObject(i) + models.add(VKConversation(source) as T) + } + + return models + } + + json.optJSONObject("response")?.let { r -> + VKConversation.conversationsCount = r.optInt("count") + } + + for (i in 0 until array.length()) { + response as JSONObject + + val source = array.optJSONObject(i) + val oConversation = source.optJSONObject("conversation") ?: return null + val oLastMessage = source.optJSONObject("last_message") ?: return null + + val conversation = VKConversation(oConversation).also { + it.lastMessage = VKMessage(oLastMessage) + } + + response.optJSONArray("profiles")?.let { + val profiles = arrayListOf() + + for (j in 0 until it.length()) { + profiles.add(VKUser(it.optJSONObject(j))) + } + + VKConversation.profiles = profiles + } + + response.optJSONArray("groups")?.let { + val groups = arrayListOf() + + for (j in 0 until it.length()) { + groups.add(VKGroup(it.optJSONObject(j))) + } + + VKConversation.groups = groups + } + + models.add(conversation as T) + } + } + } + + return models + } + + fun execute(url: String, cls: Class, listener: OnResponseListener?) { + TaskManager.execute { + try { + val models = execute(url, cls) + + listener?.let { + AppGlobal.handler.post(SuccessCallback(listener, models as E)) + } + } catch (e: Exception) { + e.printStackTrace() + + listener?.let { + AppGlobal.handler.post(ErrorCallback(listener, e)) + } + } + } + } + + fun executeArray(url: String, cls: Class, listener: OnResponseListener>?) { + TaskManager.execute { + try { + val models = execute(url, cls) + + listener?.let { + AppGlobal.handler.post(SuccessArrayCallback(listener, models as ArrayList)) + } + } catch (e: Exception) { + e.printStackTrace() + + listener?.let { + AppGlobal.handler.post(ErrorCallback(listener, e)) + } + } + } + } + + private fun optItems(source: JSONObject): JSONArray? { + val response = source.opt("response") + + return when (response) { + is JSONArray -> response + is JSONObject -> response.optJSONArray("items") + else -> null + } + } + + private fun checkError(json: JSONObject, url: String) { + if (json.has("error")) { + val error = json.optJSONObject("error") ?: return + + val code = error.optInt("error_code", -1) + val message = error.optString("error_msg", "") + val e = VKException(url, message, code) + + if (code == 5 && message.contains("invalid session")) { + context?.startActivity(Intent(context, DropUserDataActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + + if (code == ErrorCodes.CAPTCHA_NEEDED) { + e.captchaImg = error.optString("captcha_img") + e.captchaSid = error.optString("captcha_sid") + } + + if (code == ErrorCodes.VALIDATION_REQUIRED) { + e.redirectUri = error.optString("redirect_uri") + } + + throw e + } + } + + fun users(): VKUsers { + return VKUsers() + } + + fun friends(): VKFriends { + return VKFriends() + } + + fun messages(): VKMessages { + return VKMessages() + } + + fun groups(): VKGroups { + return VKGroups() + } + + fun account(): VKAccounts { + return VKAccounts() + } + + class VKFriends { + fun get(): MethodSetter { + return MethodSetter("friends.get") + } + } + + class VKUsers { + fun get(): UserMethodSetter { + return UserMethodSetter("users.get") + } + } + + class VKMessages { + fun get(): MessageMethodSetter { + return MessageMethodSetter("messages.get") + } + + fun getConversations(): MessageMethodSetter { + return MessageMethodSetter("messages.getConversations") + } + + fun getConversationsById(): MessageMethodSetter { + return MessageMethodSetter("messages.getConversationsById") + } + + fun getById(): MessageMethodSetter { + return MessageMethodSetter("messages.getById") + } + + fun search(): MessageMethodSetter { + return MessageMethodSetter("messages.search") + } + + fun getHistory(): MessageMethodSetter { + return MessageMethodSetter("messages.getHistory") + } + + fun getHistoryAttachments(): MessageMethodSetter { + return MessageMethodSetter("messages.getHistoryAttachments") + } + + fun send(): MessageMethodSetter { + return MessageMethodSetter("messages.send") + } + + fun sendSticker(): MessageMethodSetter { + return MessageMethodSetter("messages.sendSticker") + } + + fun delete(): MessageMethodSetter { + return MessageMethodSetter("messages.delete") + } + + fun deleteDialog(): MessageMethodSetter { + return MessageMethodSetter("messages.deleteDialog") + } + + fun restore(): MessageMethodSetter { + return MessageMethodSetter("messages.restore") + } + + fun markAsRead(): MessageMethodSetter { + return MessageMethodSetter("messages.markAsRead") + } + + fun markAsImportant(): MessageMethodSetter { + return MessageMethodSetter("messages.markAsImportant") + } + + fun getLongPollServer(): MessageMethodSetter { + return MessageMethodSetter("messages.getLongPollServer") + } + + /** + * Returns updates in user's private messages. + * To speed up handling of private messages, + * it can be useful to cache previously loaded messages on + * a user's mobile device/desktop, to prevent re-receipt at each call. + * With this method, you can synchronize a local copy of + * the message list with the actual version. + * + * + * Result: + * Returns an object that contains the following fields: + * 1 — history: An array similar to updates field returned + * from the Long Poll server, + * with these exceptions: + * - For events with code 4 (addition of a new message), + * there are no fields except the first three. + * - There are no events with codes 8, 9 (friend goes online/offline) + * or with codes 61, 62 (typing during conversation/chat). + * + * + * 2 — messages: An array of private message objects that were found + * among events with code 4 (addition of a new message) + * from the history field. + * Each object of message contains a set of fields described here. + * The first array element is the total number of messages + */ + fun getLongPollHistory(): MessageMethodSetter { + return MessageMethodSetter("messages.getLongPollHistory") + } + + fun getChat(): MessageMethodSetter { + return MessageMethodSetter("messages.getChat") + } + + fun createChat(): MessageMethodSetter { + return MessageMethodSetter("messages.createChat") + } + + fun editChat(): MessageMethodSetter { + return MessageMethodSetter("messages.editChat") + } + + val chatUsers: MessageMethodSetter + get() = MessageMethodSetter("messages.getChatUsers") + + fun setActivity(): MessageMethodSetter { + return MessageMethodSetter("messages.setActivity").type(true) + } + + fun addChatUser(): MessageMethodSetter { + return MessageMethodSetter("messages.addChatUser") + } + + fun removeChatUser(): MessageMethodSetter { + return MessageMethodSetter("messages.removeChatUser") + } + } + + class VKGroups { + fun getById(): MethodSetter { + return MethodSetter("groups.getById") + } + + fun join(): MethodSetter { + return MethodSetter("groups.join") + } + } + + class VKAccounts { + fun setOffline(): MethodSetter { + return MethodSetter("account.setOffline") + } + + fun setOnline(): MethodSetter { + return MethodSetter("account.setOnline") + } + } + + class SuccessCallback( + private val listener: OnResponseListener?, + private val response: E + ) : Runnable { + override fun run() { + listener?.onResponse(response) + } + } + + class SuccessArrayCallback( + private val listener: OnResponseListener>?, + private val response: ArrayList + ) : Runnable { + override fun run() { + listener?.onResponse(response) + } + } + + class ErrorCallback( + private val listener: OnResponseListener?, + private val exception: Exception + ) : Runnable { + override fun run() { + listener?.onError(exception) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt new file mode 100644 index 00000000..6c4f7a13 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKApiKeys.kt @@ -0,0 +1,15 @@ +package ru.melod1n.project.vkm.api + +enum class VKApiKeys { + READ_MESSAGE, + RESTORE_MESSAGE, + UPDATE_MESSAGE, + NEW_MESSAGE, + EDIT_MESSAGE, + DELETE_MESSAGE, + UPDATE_CONVERSATION, + + UPDATE_USER, + + UPDATE_GROUP +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt new file mode 100644 index 00000000..0e0b7442 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKAuth.kt @@ -0,0 +1,59 @@ +package ru.melod1n.project.vkm.api + +import android.util.Log +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.api.util.VKUtil +import java.net.URLEncoder + +object VKAuth { + + private const val TAG = "VKM.VKAuth" + + var redirectUrl = "https://oauth.vk.com/blank.html" + + fun getDirectAuthUrl(login: String, password: String, captcha: String = ""): String { + return "https://oauth.vk.com/token?grant_type=password&" + + "client_id=2274003&" + + "scope=notify,friends,photos,audio,video,docs,notes,pages,status,offers,questions,wall,groups,messages,email,notifications,stats,ads,market,offline&" + + "client_secret=hHbZxrka2uZ6jB1inYsH&" + + "username=$login&" + + "password=$password" + + (if (captcha.isEmpty()) "" else "&$captcha") + + "&v=${VKApi.API_VERSION}" + } + + fun getUrl(api_id: String, settings: String): String { + return "https://oauth.vk.com/authorize?" + + "client_id=$api_id&" + + "display=mobile&" + + "scope=$settings&" + + "redirect_uri=${ + URLEncoder.encode( + redirectUrl, + "utf-8" + ) + }&" + + "response_type=token&" + + "v=${URLEncoder.encode(VKApi.API_VERSION, "utf-8")}" + } + + const val settings = + "notify,friends,photos,audio,video,docs,status,notes,pages,wall,groups,messages,offline,notifications" + + @Throws(Exception::class) + fun parseRedirectUrl(url: String): Array { + val accessToken = VKUtil.extractPattern(url, "access_token=(.*?)&") + val userId = VKUtil.extractPattern(url, "user_id=(\\d*)") + + if (BuildConfig.DEBUG) { + Log.i(TAG, "access_token=$accessToken") + Log.i(TAG, "user_id=$userId") + } + + if (userId == null || userId.isEmpty() || accessToken == null || accessToken.isEmpty()) throw Exception( + "Failed to parse redirect url $url" + ) + + return arrayOf(accessToken, userId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt new file mode 100644 index 00000000..65ccba46 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKException.kt @@ -0,0 +1,14 @@ +package ru.melod1n.project.vkm.api + +import java.io.IOException + +class VKException(var url: String, override var message: String, var code: Int) : IOException(message) { + var captchaSid: String? = null + var captchaImg: String? = null + var redirectUri: String? = null + + override fun toString(): String { + return "code: $code, message: $message" + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt b/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt new file mode 100644 index 00000000..ca0dc097 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/VKLongPollParser.kt @@ -0,0 +1,146 @@ +package ru.melod1n.project.vkm.api + +import android.util.Log +import androidx.annotation.WorkerThread +import org.json.JSONArray +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.event.EventInfo + +@Suppress("UNCHECKED_CAST") +class VKLongPollParser { + + companion object { + + @WorkerThread + fun parse(updates: JSONArray) { + if (updates.length() == 0) { + return + } + + for (i in 0 until updates.length()) { + val item = updates.optJSONArray(i) + when (item.optInt(0)) { + 2 -> messageSetFlags(item) + 3 -> messageClearFlags(item) + 4 -> messageEvent(item) + 5 -> messageEdit(item) + } + } + } + + private const val TAG = "VKLongPollParser" + + private fun messageEvent(item: JSONArray) { + val message = VKUtil.parseLongPollMessage(item) + + TaskManager.execute { + if (message.isFromUser()) { + VKUtil.searchUser(message.fromId)?.let { message.fromUser = it } + } else { + VKUtil.searchGroup(message.fromId)?.let { message.fromGroup = it } + } + + MemoryCache.getConversationById(message.peerId)?.let { + it.lastMessage = message + it.lastMessageId = message.messageId + + MemoryCache.put(it) + } + + MemoryCache.put(message) + + val info = EventInfo(VKApiKeys.NEW_MESSAGE, message) + + sendEvent(info) + } + } + + private fun messageEdit(item: JSONArray) { + val message = VKUtil.parseLongPollMessage(item) + val info = EventInfo(VKApiKeys.EDIT_MESSAGE, message) + + MemoryCache.put(message) + + sendEvent(info) + } + + private fun messageDelete(item: JSONArray) { + val messageId = item.optInt(1) + val peerId = item.optInt(3) + val info = EventInfo(VKApiKeys.DELETE_MESSAGE, arrayOf(peerId, messageId)) + + MemoryCache.deleteMessage(messageId) + + sendEvent(info) + } + + private fun messageRestored(item: JSONArray) { + val message = VKUtil.parseLongPollMessage(item) + val info = EventInfo(VKApiKeys.RESTORE_MESSAGE, message) + + MemoryCache.put(message) + + sendEvent(info) + } + + private fun messageRead(item: JSONArray) { + val messageId = item.optInt(1) + val peerId = item.optInt(3) + val info = EventInfo(VKApiKeys.READ_MESSAGE, arrayOf(peerId, messageId)) + + MemoryCache.edit(MemoryCache.getMessageById(messageId)?.apply { isRead = true }) + + sendEvent(info) + } + + private fun messageClearFlags(item: JSONArray) { + val id = item.optInt(1) + val flags = item.optInt(2) + if (VKUtil.isMessageHasFlag(flags, "cancel_spam")) { + Log.i(TAG, "Message with id $id: Not spam") + } + if (VKUtil.isMessageHasFlag(flags, "deleted")) { + messageRestored(item) + } + if (VKUtil.isMessageHasFlag(flags, "important")) { + Log.i(TAG, "Message with id $id: Not Important") + } + if (VKUtil.isMessageHasFlag(flags, "unread")) { + messageRead(item) + } + } + + private fun messageSetFlags(item: JSONArray) { + val id = item.optInt(1) + val flags = item.optInt(2) + if (VKUtil.isMessageHasFlag(flags, "delete_for_all")) { + messageDelete(item) + } + if (VKUtil.isMessageHasFlag(flags, "deleted")) { + messageDelete(item) + } + if (VKUtil.isMessageHasFlag(flags, "spam")) { + Log.i(TAG, "Message with id $id: Spam") + } + if (VKUtil.isMessageHasFlag(flags, "important")) { + Log.i(TAG, "Message with id $id: Important") + } + } + + private fun sendEvent(eventInfo: EventInfo<*>) { + TaskManager.sendEvent(eventInfo) + } + } + + interface OnMessagesListener { + fun onNewMessage(message: VKMessage) + fun onEditMessage(message: VKMessage) + fun onReadMessage(messageId: Int, peerId: Int) + fun onDeleteMessage(messageId: Int, peerId: Int) + fun onRestoredMessage(message: VKMessage) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt new file mode 100644 index 00000000..06f56c43 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/MessageMethodSetter.kt @@ -0,0 +1,205 @@ +package ru.melod1n.project.vkm.api.method + +import ru.melod1n.project.vkm.util.ArrayUtils + +class MessageMethodSetter(name: String) : MethodSetter(name) { + + fun out(value: Boolean): MessageMethodSetter { + put("out", value) + return this + } + + fun timeOffset(value: Int): MessageMethodSetter { + put("time_offset", value) + return this + } + + fun filters(value: Int): MessageMethodSetter { + put("filters", value) + return this + } + + fun previewLength(value: Int): MessageMethodSetter { + put("preview_length", value) + return this + } + + fun lastMessageId(value: Int): MessageMethodSetter { + put("last_message_id", value) + return this + } + + fun unread(value: Boolean): MessageMethodSetter { + put("unread", value) + return this + } + + fun messageIds(vararg ids: Int): MessageMethodSetter { + put("message_ids", ArrayUtils.asString(ids)) + return this + } + + fun messageIds(ids: ArrayList): MessageMethodSetter { + put("message_ids", ArrayUtils.asString(ids)) + return this + } + + fun q(query: String): MessageMethodSetter { + put("q", query) + return this + } + + fun startMessageId(id: Int): MessageMethodSetter { + put("start_message_id", id) + return this + } + + fun peerId(value: Int): MessageMethodSetter { + put("peer_id", value) + return this + } + + fun peerIds(vararg values: Int): MessageMethodSetter { + put("peer_ids", ArrayUtils.asString(values)) + return this + } + + fun reversed(value: Boolean): MessageMethodSetter { + put("rev", value) + return this + } + + fun domain(value: String): MessageMethodSetter { + put("domain", value) + return this + } + + fun chatId(value: Int): MessageMethodSetter { + put("chat_id", value) + return this + } + + fun message(message: String): MessageMethodSetter { + put("message", message) + return this + } + + fun randomId(value: Int): MessageMethodSetter { + put("random_id", value) + return this + } + + fun lat(lat: Double): MessageMethodSetter { + put("lat", lat) + return this + } + + fun longitude(value: Long): MessageMethodSetter { + put("LONG", value) + return this + } + + fun attachment(attachments: Collection): MessageMethodSetter { + put("attachment", ArrayUtils.asString(attachments)) + return this + } + + fun attachment(vararg attachments: String): MessageMethodSetter { + put("attachment", ArrayUtils.asString(*attachments)) + return this + } + + fun forwardMessages(ids: Collection): MessageMethodSetter { + put("forward_messages", ArrayUtils.asString(ids)) + return this + } + + fun forwardMessages(vararg ids: Int): MessageMethodSetter { + put("forward_messages", ArrayUtils.asString(ids)) + return this + } + + fun stickerId(value: Int): MessageMethodSetter { + put("sticker_id", value) + return this + } + + fun messageId(value: Int): MessageMethodSetter { + put("message_id", value) + return this + } + + fun important(value: Boolean): MessageMethodSetter { + put("important", value) + return this + } + + fun ts(value: Long): MessageMethodSetter { + put("ts", value) + return this + } + + fun pts(value: Int): MessageMethodSetter { + put("pts", value) + return this + } + + fun msgsLimit(limit: Int): MessageMethodSetter { + put("msgs_limit", limit) + return this + } + + fun onlines(onlines: Boolean): MessageMethodSetter { + put("onlines", onlines) + return this + } + + fun maxMsgId(id: Int): MessageMethodSetter { + put("max_msg_id", id) + return this + } + + fun chatIds(vararg ids: Int): MessageMethodSetter { + put("max_msg_id", ArrayUtils.asString(ids)) + return this + } + + fun chatIds(ids: Collection): MessageMethodSetter { + put("max_msg_id", ArrayUtils.asString(ids)) + return this + } + + fun title(title: String): MessageMethodSetter { + put("title", title) + return this + } + + fun type(typing: Boolean): MessageMethodSetter { + if (typing) { + put("type", "typing") + } + return this + } + + fun mediaType(type: String): MessageMethodSetter { + put("media_type", type) + return this + } + + fun photoSizes(value: Boolean): MessageMethodSetter { + return put("photo_sizes", value) as MessageMethodSetter + } + + fun filter(value: String): MessageMethodSetter { + return put("filter", value) as MessageMethodSetter + } + + fun extended(value: Boolean): MessageMethodSetter { + return put("extended", value) as MessageMethodSetter + } + + fun markConversationAsRead(asRead: Boolean): MessageMethodSetter { + put("mark_conversation_as_read", asRead) + return this + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt new file mode 100644 index 00000000..0c7405e5 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/MethodSetter.kt @@ -0,0 +1,166 @@ +package ru.melod1n.project.vkm.api.method + +import android.util.ArrayMap +import android.util.Log +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.util.ArrayUtils +import java.net.URLEncoder + +@Suppress("UNCHECKED_CAST") +open class MethodSetter(private val name: String) { + + private val params: ArrayMap = ArrayMap() + + fun put(key: String, value: Any): MethodSetter { + params[key] = value.toString() + return this + } + + fun put(key: String, value: String): MethodSetter { + params[key] = value + return this + } + + fun put(key: String, value: Int): MethodSetter { + params[key] = value.toString() + return this + } + + fun put(key: String, value: Long): MethodSetter { + params[key] = value.toString() + return this + } + + fun put(key: String, value: Boolean): MethodSetter { + params[key] = if (value) "1" else "0" + return this + } + + private fun getSignedUrl(): String { + if (!params.containsKey("access_token")) { + params["access_token"] = UserConfig.token + } + + if (!params.containsKey("v")) { + params["v"] = VKApi.API_VERSION + } + + if (!params.containsKey("lang")) { + params["lang"] = VKApi.language + } + + return "${VKApi.BASE_URL}$name?${retrieveParams()}" + } + + private fun retrieveParams(): String { + val builder = StringBuilder() + + for (i in 0 until params.size) { + val key = params.keyAt(i) + val value = params.valueAt(i) + + if (builder.isNotEmpty()) { + builder.append("&") + } + + builder.append(key) + builder.append("=") + builder.append(URLEncoder.encode(value, "UTF-8")) + } + + val params = builder.toString() + + if (BuildConfig.DEBUG) { + Log.i("MethodSetter", "retrieved params: $params") + } + + return params + } + + fun execute(cls: Class): ArrayList? { + return VKApi.execute(getSignedUrl(), cls) + } + + fun executeArray(cls: Class, listener: OnResponseListener>?) { + VKApi.executeArray(getSignedUrl(), cls, listener) + } + + fun execute(cls: Class, listener: OnResponseListener?) { + VKApi.execute(getSignedUrl(), cls, listener) + } + + fun userId(value: Int): MethodSetter { + return put("user_id", value) + } + + fun userIds(vararg ids: Int): MethodSetter { + return put("user_ids", ArrayUtils.asString(ids)) + } + + fun userIds(ids: ArrayList): MethodSetter { + return put("user_ids", ArrayUtils.asString(ids)) + } + + fun ownerId(value: Int): MethodSetter { + return put("owner_id", value) + } + + fun groupId(value: Int): MethodSetter { + return put("group_id", value) + } + + fun groupIds(vararg ids: Int): MethodSetter { + return put("group_ids", ArrayUtils.asString(ids)) + } + + fun groupIds(ids: ArrayList): MethodSetter { + return put("group_ids", ArrayUtils.asString(ids)) + } + + fun fields(values: String): MethodSetter { + return put("fields", values) + } + + fun count(value: Int): MethodSetter { + return put("count", value) + } + + fun sort(value: Int): MethodSetter { + put("sort", value) + return this + } + + /** + * + * hints — сортировать по рейтингу, аналогично тому, как друзья сортируются в разделе Мои друзья + * random — возвращает друзей в случайном порядке. + * mobile — возвращает выше тех друзей, у которых установлены мобильные приложения. + * name — сортировать по имени (долго) + * + */ + + fun order(value: String): MethodSetter { + put("order", value) + return this + } + + fun offset(value: Int = 0): MethodSetter { + return put("offset", value) + } + + fun nameCase(value: String): MethodSetter { + return put("name_case", value) + } + + fun captchaSid(value: String): MethodSetter { + return put("captcha_sid", value) + } + + fun captchaKey(value: String): MethodSetter { + return put("captcha_key", value) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt b/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt new file mode 100644 index 00000000..83137e71 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/method/UserMethodSetter.kt @@ -0,0 +1,44 @@ +package ru.melod1n.project.vkm.api.method + +class UserMethodSetter(name: String) : MethodSetter(name) { + + fun extended(extended: Boolean): UserMethodSetter { + put("extended", extended) + return this + } + + fun type(type: String): UserMethodSetter { + put("type", type) + return this + } + + fun comment(comment: String): UserMethodSetter { + put("comment", comment) + return this + } + + fun latitude(latitude: Float): UserMethodSetter { + put("latitude", latitude) + return this + } + + fun longitude(longitude: Float): UserMethodSetter { + put("longitude", longitude) + return this + } + + fun accuracy(accuracy: Int): UserMethodSetter { + put("accuracy", accuracy) + return this + } + + fun timeout(timeout: Int): UserMethodSetter { + put("timeout", timeout) + return this + } + + fun radius(radius: Int): UserMethodSetter { + put("radius", radius) + return this + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt new file mode 100644 index 00000000..02205b99 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAttachments.kt @@ -0,0 +1,59 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONArray +import java.util.* + +object VKAttachments { + + private const val TYPE_PHOTO = "photo" + private const val TYPE_VIDEO = "video" + private const val TYPE_AUDIO = "audio" + private const val TYPE_DOC = "doc" + private const val TYPE_LINK = "link" + private const val TYPE_STICKER = "sticker" + private const val TYPE_GIFT = "gift" + private const val TYPE_AUDIO_MESSAGE = "audio_message" + private const val TYPE_GRAFFITI = "graffiti" + private const val TYPE_POLL = "poll" + private const val TYPE_GEO = "geo" + private const val TYPE_WALL = "wall" + private const val TYPE_CALL = "call" + private const val TYPE_STORY = "story" + private const val TYPE_POINT = "point" + private const val TYPE_MARKET = "market" + private const val TYPE_ARTICLE = "article" + private const val TYPE_PODCAST = "podcast" + private const val TYPE_WALL_REPLY = "wall_reply" + private const val TYPE_MONEY_REQUEST = "money_request" + private const val TYPE_AUDIO_PLAYLIST = "audio_playlist" + + fun parse(array: JSONArray): ArrayList { + val attachments = ArrayList(array.length()) + + for (i in 0 until array.length()) { + var attachment = array.optJSONObject(i) ?: continue + if (attachment.has("attachment")) { + attachment = attachment.optJSONObject("attachment") ?: continue + } + + val type = attachment.optString("type") + val jsonObject = attachment.optJSONObject(type) ?: continue + + when (type) { + TYPE_PHOTO -> attachments.add(VKPhoto(jsonObject)) + TYPE_AUDIO -> attachments.add(VKAudio(jsonObject)) + TYPE_VIDEO -> attachments.add(VKVideo(jsonObject)) + TYPE_DOC -> attachments.add(VKDoc(jsonObject)) + TYPE_STICKER -> attachments.add(VKSticker(jsonObject)) + TYPE_LINK -> attachments.add(VKLink(jsonObject)) + TYPE_GIFT -> attachments.add(VKGift(jsonObject)) + TYPE_AUDIO_MESSAGE -> attachments.add(VKAudioMessage(jsonObject)) + TYPE_GRAFFITI -> attachments.add(VKGraffiti(jsonObject)) + TYPE_POLL -> attachments.add(VKPoll(jsonObject)) + TYPE_CALL -> attachments.add(VKCall(jsonObject)) + } + } + + return attachments + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt new file mode 100644 index 00000000..dd44a9d6 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudio.kt @@ -0,0 +1,17 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKAudio(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var ownerId = o.optInt("owner_id", -1) + var artist: String = o.optString("artist") + var title: String = o.optString("title") + var duration = o.optInt("duration") + var url: String = o.optString("url") + var date = o.optInt("date") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt new file mode 100644 index 00000000..03e3c877 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKAudioMessage.kt @@ -0,0 +1,23 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKAudioMessage(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var duration = o.optInt("duration") + var waveform = ArrayList() + var linkOgg: String = o.optString("link_ogg") + var linkMp3: String = o.optString("link_mp3") + + init { + o.optJSONArray("waveform")?.let { + val waveform = ArrayList() + for (i in 0 until it.length()) { + waveform.add(it.optInt(i)) + } + this.waveform = waveform + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt new file mode 100644 index 00000000..d43294fb --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKCall.kt @@ -0,0 +1,15 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKCall(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var initiatorId = o.optInt("initiator_id", -1) + var receiverId = o.optInt("receiver_id", -1) + var state: String = o.optString("state") //reached, canceled_by_initiator, canceled_by_receiver + var time = o.optInt("time") + var duration = o.optInt("duration") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt new file mode 100644 index 00000000..84b0464b --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKComment.kt @@ -0,0 +1,4 @@ +package ru.melod1n.project.vkm.api.model + +class VKComment { //https://vk.com/dev/objects/comment +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt new file mode 100644 index 00000000..c449e5a5 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKConversation.kt @@ -0,0 +1,144 @@ +package ru.melod1n.project.vkm.api.model + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.RoomWarnings +import org.json.JSONObject + +@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED) +@Entity(tableName = "conversations") +class VKConversation() : VKModel(), Cloneable { + + companion object { + var profiles = arrayListOf() + var groups = arrayListOf() + + var conversationsCount: Int = 0 + + const val STATE_IN = "in" + const val STATE_KICKED = "kicked" + const val STATE_LEFT = "left" + + const val TYPE_USER = "user" + const val TYPE_CHAT = "chat" + const val TYPE_GROUP = "group" + + var count = 0 + } + + /* + 18 — пользователь заблокирован или удален; + 900 — нельзя отправить сообщение пользователю, который в чёрном списке; + 901 — пользователь запретил сообщения от сообщества; + 902 — пользователь запретил присылать ему сообщения с помощью настроек приватности; + 915 — в сообществе отключены сообщения; + 916 — в сообществе заблокированы сообщения; + 917 — нет доступа к чату; + 918 — нет доступа к e-mail; + 203 — нет доступа к сообществу + */ + + var isAllowed = false + var reason = -1 + + var inRead = 0 + var outRead = 0 + var lastMessageId = 0 + var unreadCount = 0 + + @PrimaryKey(autoGenerate = false) + var conversationId = 0 + + var type: String = "" + var localId = 0 + + var disabledUntil = 0 + var isDisabledForever = false + var isNoSound = false + + var membersCount = 0 + var title: String = "" + + var pinnedMessageId = 0 + + var state: String = "" + + @Embedded(prefix = "cMessage") + var lastMessage = VKMessage() + + var isGroupChannel = false + + var photo50: String = "" + var photo100: String = "" + var photo200: String = "" + + @Embedded(prefix = "cUser") + var peerUser: VKUser? = null + + @Embedded(prefix = "cGroup") + var peerGroup: VKGroup? = null + + constructor(o: JSONObject) : this() { + inRead = o.optInt("in_read") + outRead = o.optInt("out_read") + lastMessageId = o.optInt("last_message_id", -1) + unreadCount = o.optInt("unread_count", 0) + + o.optJSONObject("peer")?.let { + conversationId = it.optInt("id", -1) + type = it.optString("type") + localId = it.optInt("local_id") + } + + o.optJSONObject("push_settings")?.let { + disabledUntil = it.optInt("disabled_until") + isDisabledForever = it.optBoolean("disabled_forever") + isNoSound = it.optBoolean("no_sound") + } + + o.optJSONObject("can_write")?.let { + isAllowed = it.optBoolean("allowed") + reason = it.optInt("reason", -1) + } + + o.optJSONObject("chat_settings")?.let { + membersCount = it.optInt("members_count") + title = it.optString("title") + + it.optJSONObject("pinned_message")?.let { pinned -> + pinnedMessageId = VKPinnedMessage(pinned).id + } + + state = it.optString("state") + + it.optJSONObject("photo")?.let { photo -> + photo50 = photo.optString("photo_50") + photo100 = photo.optString("photo_100") + photo200 = photo.optString("photo_200") + } + + isGroupChannel = it.optBoolean("is_group_channel") + } + } + + fun isNotificationsDisabled() = (isDisabledForever || disabledUntil > 0 || isNoSound) + + fun isChatId() = conversationId > 2_000_000_000 + + fun isChat() = type == TYPE_CHAT + + fun isUser() = type == TYPE_USER + + fun isNotUser() = !isUser() + + fun isGroup() = type == TYPE_GROUP + + override fun toString(): String { + return title + } + + public override fun clone(): VKConversation { + return super.clone() as VKConversation + } +} diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt new file mode 100644 index 00000000..9c87a05b --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKDoc.kt @@ -0,0 +1,73 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject +import java.util.* + +class VKDoc(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + companion object { + const val TYPE_NONE = 0 + const val TYPE_TEXT = 1 + const val TYPE_ARCHIVE = 2 + const val TYPE_GIF = 3 + const val TYPE_IMAGE = 4 + const val TYPE_AUDIO = 5 + const val TYPE_VIDEO = 6 + const val TYPE_BOOK = 7 + const val TYPE_UNKNOWN = 8 + } + + var id = o.optInt("id", -1) + var ownerId = o.optInt("owner_id", -1) + var title: String = o.optString("title") + var size = o.optInt("size") + var ext: String = o.optString("ext") + var url: String = o.optString("url") + var date = o.optInt("date") + var type = o.optInt("type") + var preview: Preview? = null + + init { + o.optJSONObject("preview")?.let { + preview = Preview(it) + } + } + + class Preview(o: JSONObject) { + var photo: Photo? = null + var graffiti: Graffiti? = null + + inner class Photo(o: JSONObject) { + var sizes: ArrayList? = null + + init { + o.optJSONArray("sizes")?.let { + val sizes = ArrayList() + for (i in 0 until it.length()) { + sizes.add(VKPhotoSize(it.optJSONObject(i))) + } + this.sizes = sizes + } + } + } + + class Graffiti(o: JSONObject) { + var src: String = o.optString("src") + var width = o.optInt("width") + var height = o.optInt("height") + } + + init { + o.optJSONObject("photo")?.let { + photo = Photo(it) + } + + o.optJSONObject("graffiti")?.let { + graffiti = Graffiti(it) + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt new file mode 100644 index 00000000..67f7aa72 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKFriend.kt @@ -0,0 +1,18 @@ +package ru.melod1n.project.vkm.api.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "friends") +class VKFriend() { + + @PrimaryKey(autoGenerate = false) + var friendId: Int = -1 + + var userId: Int = -1 + + constructor(friendId: Int, userId: Int): this() { + this.friendId = friendId + this.userId = userId + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt new file mode 100644 index 00000000..9226b7fc --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGift.kt @@ -0,0 +1,14 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKGift(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var thumb256: String = o.optString("thumb_256") + var thumb96: String = o.optString("thumb_96") + var thumb48: String = o.optString("thumb_48") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt new file mode 100644 index 00000000..aebfbd6d --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGraffiti.kt @@ -0,0 +1,16 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKGraffiti(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var ownerId = o.optInt("owner_id", -1) + var url: String = o.optString("url") + var width = o.optInt("width") + var height = o.optInt("height") + var accessKey: String = o.optString("access_key") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt new file mode 100644 index 00000000..e65b657a --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKGroup.kt @@ -0,0 +1,46 @@ +package ru.melod1n.project.vkm.api.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.json.JSONArray +import org.json.JSONObject + +@Entity(tableName = "groups") +open class VKGroup(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + companion object { + const val DEFAULT_FIELDS = "description,members_count,counters,status,verified" + + fun isGroupId(id: Int): Boolean { + return id < 0 + } + + val EMPTY: VKGroup = object : VKGroup() { + init { + name = "Unknown" + } + } + + fun parse(array: JSONArray): ArrayList { + val groups = ArrayList() + + for (i in 0 until array.length()) { + groups.add(VKGroup(array.optJSONObject(i))) + } + return groups + } + } + + @PrimaryKey(autoGenerate = false) + var groupId = o.optInt("id", -1) + var name: String = o.optString("name") + var screenName: String = o.optString("screen_name") + var isClosed = o.optInt("is_closed") == 1 + var deactivated: String = o.optString("deactivated") + var type: String = o.optString("type") + var photo50: String = o.optString("photo_50") + var photo100: String = o.optString("photo_100") + var photo200: String = o.optString("photo_200") +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt new file mode 100644 index 00000000..debc463d --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLink.kt @@ -0,0 +1,46 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject +import java.io.Serializable + +class VKLink(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var url: String = o.optString("url") + var title: String = o.optString("title") + var caption: String = o.optString("caption") + var description: String = o.optString("description") + var previewPage: String = o.optString("preview_page") + var previewUrl: String = o.optString("preview_url") + var photo: VKPhoto? = null + var button: Button? = null + + init { + o.optJSONObject("photo")?.let { + photo = VKPhoto(it) + } + + o.optJSONObject("button")?.let { + button = Button(it) + } + } + + class Button(o: JSONObject) : Serializable { + var title: String = o.optString("title") + var action: Action? = null + + init { + o.optJSONObject("action")?.let { + action = Action(it) + } + } + + class Action(o: JSONObject) : Serializable { + + var type: String = o.optString("type") + var url: String = o.optString("url") + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt new file mode 100644 index 00000000..1ee940d2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollHistory.kt @@ -0,0 +1,12 @@ +package ru.melod1n.project.vkm.api.model + +import java.util.* + +class VKLongPollHistory : VKModel() { + + private val lpMessages: ArrayList? = null + private val messages: ArrayList? = null + private val profiles: ArrayList? = null + private val groups: ArrayList? = null //TODO: использовать + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt new file mode 100644 index 00000000..cffc1834 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKLongPollServer.kt @@ -0,0 +1,13 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKLongPollServer(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var key: String = o.optString("key") + var server = o.optString("server").replace("\\", "") + var ts: Long = o.optLong("ts") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt new file mode 100644 index 00000000..66392ea8 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketAlbum.kt @@ -0,0 +1,4 @@ +package ru.melod1n.project.vkm.api.model + +class VKMarketAlbum { //https://vk.com/dev/objects/market_album +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt new file mode 100644 index 00000000..04986485 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMarketItem.kt @@ -0,0 +1,4 @@ +package ru.melod1n.project.vkm.api.model + +class VKMarketItem { //https://vk.com/dev/objects/market_item +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt new file mode 100644 index 00000000..3f63df31 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessage.kt @@ -0,0 +1,177 @@ +package ru.melod1n.project.vkm.api.model + +import android.util.ArrayMap +import androidx.room.* +import org.json.JSONObject +import ru.melod1n.project.vkm.database.dao.converters.ArrayListToByteArrayConverter +import ru.melod1n.project.vkm.database.dao.converters.ForwardedConverter + +@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED) +@Entity(tableName = "messages") +open class VKMessage() : VKModel() { + + companion object { + + var profiles = arrayListOf() + var groups = arrayListOf() + var conversations = arrayListOf() + + const val serialVersionUID = 1L + + var lastHistoryCount = 0 + + const val UNREAD = 1 // Оно просто есть + const val OUTBOX = 1 shl 1 // Исходящее сообщение + const val REPLIED = 1 shl 2 // На сообщение был создан ответ + const val IMPORTANT = 1 shl 3 // Важное сообщение + const val FRIENDS = 1 shl 5 // Сообщение в чат друга + const val SPAM = 1 shl 6 // Сообщение помечено как спам + const val DELETED = 1 shl 7 // Удаление сообщения + const val AUDIO_LISTENED = 1 shl 12 // ГС прослушано + const val CHAT = 1 shl 13 // Сообщение отправлено в беседу + const val CANCEL_SPAM = 1 shl 15 // Отмена пометки спама + const val HIDDEN = 1 shl 16 // Приветственное сообщение сообщества + const val DELETE_FOR_ALL = 1 shl 17 // Сообщение удалено для всех + const val CHAT_IN = 1 shl 19 // Входящее сообщение в беседе + const val REPLY_MSG = 1 shl 21 // Ответ на сообщение + + val flags = ArrayMap() + + fun isOut(flags: Int): Boolean { + return OUTBOX and flags > 0 + } + + fun isDeleted(flags: Int): Boolean { + return DELETED and flags > 0 + } + + fun isUnread(flags: Int): Boolean { + return UNREAD and flags > 0 + } + + fun isSpam(flags: Int): Boolean { + return SPAM and flags > 0 + } + + fun isCanceledSpam(flags: Int): Boolean { + return CANCEL_SPAM and flags > 0 + } + + fun isImportant(flags: Int): Boolean { + return IMPORTANT and flags > 0 + } + + fun isDeletedForAll(flags: Int): Boolean { + return DELETE_FOR_ALL and flags > 0 + } + + init { + flags["unread"] = UNREAD + flags["outbox"] = OUTBOX + flags["replied"] = REPLIED + flags["important"] = IMPORTANT + flags["friends"] = FRIENDS + flags["spam"] = SPAM + flags["deleted"] = DELETED + flags["audio_listened"] = AUDIO_LISTENED + flags["chat"] = CHAT + flags["cancel_spam"] = CANCEL_SPAM + flags["hidden"] = HIDDEN + flags["delete_for_all"] = DELETE_FOR_ALL + flags["chat_in"] = CHAT_IN + flags["reply_msg"] = REPLY_MSG + } + } + + @PrimaryKey(autoGenerate = false) + var messageId = 0 + var date = 0 + var peerId = 0 + var fromId = 0 + var editTime = 0 + var isOut = false + var text: String = "" + var randomId = 0 + var conversationMessageId = 0 + + var hasEmoji = false + var isImportant = false + var isRead = false + + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + @TypeConverters(ArrayListToByteArrayConverter::class) + var attachments: ArrayList = arrayListOf() + + // @Ignore + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + @TypeConverters(ForwardedConverter::class) + var fwdMessages: ArrayList = arrayListOf() + + var replyMessageId = 0 + + @Embedded(prefix = "mAction") + var action: VKMessageAction? = null + + @Embedded(prefix = "mUser") + var fromUser: VKUser? = null + + @Embedded(prefix = "Group") + var fromGroup: VKGroup? = null + + constructor(o: JSONObject) : this() { + messageId = o.optInt("id", -1) + date = o.optInt("date") + peerId = o.optInt("peer_id", -1) + fromId = o.optInt("from_id", -1) + editTime = o.optInt("edit_time", -1) + isOut = o.optInt("out") == 1 + text = o.optString("text") + randomId = o.optInt("random_id", -1) + conversationMessageId = o.optInt("conversation_message_id", -1) + isImportant = o.optBoolean("important") + + o.optJSONArray("attachments")?.let { + attachments = VKAttachments.parse(it) + } + + o.optJSONArray("fwd_messages")?.let { + val fwdMessages = ArrayList(it.length()) + for (i in 0 until it.length()) { + fwdMessages.add(VKMessage(it.optJSONObject(i))) + } + this.fwdMessages = fwdMessages + } + + o.optJSONObject("reply_message")?.let { + replyMessageId = VKMessage(it).messageId + } + + o.optJSONObject("action")?.let { + action = VKMessageAction(it) + } + } + + fun getForwardedMessages() = ArrayList().apply { + for (model in fwdMessages) add(model as VKMessage) + } + + fun isFromUser() = fromId > 0 + + fun isFromGroup() = fromId < 0 + + fun isOutbox() = isOut + + fun isInbox() = !isOutbox() + + fun isReaded() = isRead + + fun isUnreaded() = !isReaded() + + override fun toString(): String { + return if (text.isNotEmpty()) { + text + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt new file mode 100644 index 00000000..154c9ad3 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKMessageAction.kt @@ -0,0 +1,57 @@ +package ru.melod1n.project.vkm.api.model + +import androidx.room.Ignore +import org.json.JSONObject + +class VKMessageAction() : VKModel() { + + companion object { + const val ACTION_CHAT_CREATE = "chat_create" + const val ACTION_PHOTO_UPDATE = "chat_photo_update" + const val ACTION_PHOTO_REMOVE = "chat_photo_remove" + const val ACTION_TITLE_UPDATE = "chat_title_update" + const val ACTION_PIN_MESSAGE = "chat_pin_message" + const val ACTION_UNPIN_MESSAGE = "chat_unpin_message" + const val ACTION_INVITE_USER = "chat_invite_user" + const val ACTION_INVITE_USER_BY_LINK = "chat_invite_user_by_link" + const val ACTION_KICK_USER = "chat_kick_user" + const val ACTION_SCREENSHOT = "chat_screenshot" + const val ACTION_INVITE_USER_BY_CALL = "chat_invite_user_by_call" + const val ACTION_INVITE_USER_BY_CALL_JOIN_LINK = "chat_invite_user_by_call_link" + } + + /* + chat_photo_update — обновлена фотография беседы; + chat_photo_remove — удалена фотография беседы; + chat_create — создана беседа; + chat_title_update — обновлено название беседы; + chat_invite_user — приглашен пользователь; + chat_kick_user — исключен пользователь; + chat_pin_message — закреплено сообщение; + chat_unpin_message — откреплено сообщение; + chat_invite_user_by_link — пользователь присоединился к беседе по ссылке. + */ + + var type: String = "" + + var memberId = 0 + + @Ignore + var message: VKMessage? = null + + var conversationMessageId = 0 + + var text: String = "" + var oldText: String = "" + +// @Embedded(prefix = "photo") +// var photo: Photo? = null + + //TODO: add photo + + constructor(o: JSONObject) : this() { + type = o.optString("type") + memberId = o.optInt("member_id", -1) + text = o.optString("text") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt new file mode 100644 index 00000000..ecdfdaa2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKModel.kt @@ -0,0 +1,9 @@ +package ru.melod1n.project.vkm.api.model + +import java.io.Serializable + +abstract class VKModel : Serializable { + companion object { + const val serialVersionUID = 1L + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt new file mode 100644 index 00000000..9ceff1d5 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhoto.kt @@ -0,0 +1,28 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject +import java.util.* + +class VKPhoto(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var albumId = o.optInt("album_id", -1) + var ownerId = o.optInt("owner_id", -1) + var text: String = o.optString("text") + var date = o.optInt("date") + var width = o.optInt("width") + var height = o.optInt("height") + var sizes: ArrayList? = null + + init { + o.optJSONArray("sizes")?.let { + val sizes = ArrayList() + for (i in 0 until it.length()) { + sizes.add(VKPhotoSize(it.optJSONObject(i))) + } + this.sizes = sizes + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt new file mode 100644 index 00000000..ce1d23f0 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPhotoSize.kt @@ -0,0 +1,14 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKPhotoSize(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var type: String = o.optString("type") + var url: String = o.optString("url") + var height = o.optInt("height") + var width = o.optInt("width") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt new file mode 100644 index 00000000..c61176ec --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPinnedMessage.kt @@ -0,0 +1,25 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject +import java.util.* + +class VKPinnedMessage(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var date = o.optInt("date") + var fromId = o.optInt("from_id", -1) + var text: String = o.optString("text") + var attachments: ArrayList? = null + var fwdMessages: ArrayList? = null + + init { + o.optJSONArray("attachments")?.let { + attachments = VKAttachments.parse(it) + } + + //TODO: parse forwarded + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt new file mode 100644 index 00000000..9ba066d4 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKPoll.kt @@ -0,0 +1,55 @@ +package ru.melod1n.project.vkm.api.model + +import android.graphics.Color +import org.json.JSONObject +import java.io.Serializable +import java.util.* + +class VKPoll(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var ownerId = o.optInt("owner_id", -1) + var created = o.optInt("created") + var question: String = o.optString("question") + var votes = o.optInt("votes") + var answers = ArrayList() + var isAnonymous = o.optBoolean("anonymous") + var isMultiple = o.optBoolean("multiple") + var answerIds = ArrayList() + var endDate = o.optInt("end_date") + var isClosed = o.optBoolean("closed") + var isBoard = o.optBoolean("is_board") + var isCanEdit = o.optBoolean("can_edit") + var isCanVote = false + var isCanReport = false + var isCanShare = false + var authorId = 0 + var background = Color.WHITE + + //TODO: private ArrayList friends + + init { + o.optJSONArray("answers")?.let { + val answers = ArrayList() + for (i in 0 until it.length()) { + answers.add(Answer(it.optJSONObject(i))) + } + this.answers = answers + } + + //setAnswerIds(); + + // ... + } + + class Answer(o: JSONObject) : Serializable { + + var id = o.optInt("id", -1) + var text: String = o.optString("text") + var votes = o.optInt("votes") + var rate = o.optInt("rate") + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt new file mode 100644 index 00000000..4dcadd73 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKSticker.kt @@ -0,0 +1,31 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject +import java.util.* + +class VKSticker(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var productId = o.optInt("product_id", -1) + var stickerId = o.optInt("sticker_id", -1) + var images: ArrayList? = null + + init { + o.optJSONArray("images")?.let { + val images = ArrayList() + for (i in 0 until it.length()) { + images.add(Image(it.optJSONObject(i))) + } + this.images = images + } + } + + class Image(o: JSONObject) : VKModel() { + + var url: String = o.optString("url") + var width = o.optInt("width") + var height = o.optInt("height") + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt new file mode 100644 index 00000000..37918ae3 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKUser.kt @@ -0,0 +1,74 @@ +package ru.melod1n.project.vkm.api.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.json.JSONArray +import org.json.JSONObject + +@Entity(tableName = "users") +open class VKUser(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + companion object { + + var friendsCount: Int = 0 + + const val DEFAULT_FIELDS = + "photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex" + + val EMPTY: VKUser = object : VKUser() { + override fun toString(): String { + return "Unknown Unknown" + } + } + + fun isUserId(id: Int): Boolean { + return id > 0 && id < 2000000000 + } + + + @JvmStatic + fun parse(array: JSONArray): ArrayList { + val users = ArrayList() + + for (i in 0 until array.length()) { + users.add(VKUser(array.optJSONObject(i))) + } + + return users + } + } + + @PrimaryKey(autoGenerate = false) + var userId = o.optInt("id", -1) + var firstName: String = o.optString("first_name") + var lastName: String = o.optString("last_name") + var deactivated: String = o.optString("deactivated") + var isClosed = o.optBoolean("is_closed") + var isCanAccessClosed = o.optBoolean("can_access_closed") + var sex = o.optInt("sex") + var screenName: String = o.optString("screen_name") + var photo50: String = o.optString("photo_50") + var photo100: String = o.optString("photo_100") + var photo200: String = o.optString("photo_200") + var isOnline = o.optInt("online") == 1 + var isOnlineMobile = isOnline && o.optInt("online_mobile") == 1 + var status: String = o.optString("status") + + var lastSeen = 0 + var lastSeenPlatform = 0 + + var isVerified = o.optInt("verified") == 1 + + init { + o.optJSONObject("last_seen")?.let { + lastSeen = it.optInt("time") + lastSeenPlatform = it.optInt("platform") + } + } + + override fun toString(): String { + return "$firstName $lastName" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt new file mode 100644 index 00000000..48595690 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKVideo.kt @@ -0,0 +1,37 @@ +package ru.melod1n.project.vkm.api.model + +import org.json.JSONObject + +class VKVideo(o: JSONObject) : VKModel() { + + constructor() : this(JSONObject()) + + var id = o.optInt("id", -1) + var ownerId = o.optInt("owner_id", -1) + var title: String = o.optString("title") + var description: String = o.optString("description") + var duration = o.optInt("duration", -1) + var photo130: String = o.optString("photo_130") + var photo320: String = o.optString("photo_320") + var photo640: String = o.optString("photo_640") + var photo800: String = o.optString("photo_800") + var photo1280: String = o.optString("photo_1280") + var firstFrame130: String = o.optString("first_frame_130") + var firstFrame320: String = o.optString("first_frame_320") + var firstFrame640: String = o.optString("first_frame_640") + var firstFrame800: String = o.optString("first_frame_800") + var firstFrame1280: String = o.optString("first_frame_1280") + var date = o.optInt("date") + var views = o.optInt("views") + var comments = o.optInt("comments") + var player: String = o.optString("player") + var isCanEdit = o.optInt("can_edit", 0) == 1 + var isCanAdd = o.optInt("can_add") == 1 + var isPrivate = o.optInt("is_private", 0) == 1 + var accessKey: String = o.optString("access_key") + var isProcessing = o.optInt("processing", 0) == 1 + var isLive = o.optInt("live", 0) == 1 + var isUpcoming = o.optInt("upcoming", 0) == 1 + var isFavorite = o.optBoolean("favorite") + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt new file mode 100644 index 00000000..0cc6a244 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/model/VKWall.kt @@ -0,0 +1,4 @@ +package ru.melod1n.project.vkm.api.model + +class VKWall { //https://vk.com/dev/objects/post +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt b/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt new file mode 100644 index 00000000..0751c3ce --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/api/util/VKUtil.kt @@ -0,0 +1,680 @@ +package ru.melod1n.project.vkm.api.util + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.core.content.ContextCompat +import com.amulyakhare.textdrawable.TextDrawable +import org.json.JSONArray +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.model.* +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.StringExtensions.lowerCase +import ru.melod1n.project.vkm.listener.OnResponseListener +import ru.melod1n.project.vkm.util.TextUtils +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern +import kotlin.math.abs + +object VKUtil { + + private const val TAG = "VKM: VKUtil" + + fun extractPattern(string: String, pattern: String): String? { + val p = Pattern.compile(pattern) + val m = p.matcher(string) + return if (!m.find()) null else m.toMatchResult().group(1) + } + + private const val pattern_string_profile_id = "^(id)?(\\d{1,10})$" + + private val pattern_profile_id = Pattern.compile(pattern_string_profile_id) + + fun parseProfileId(text: String): String? { + val m = pattern_profile_id.matcher(text) + return if (!m.find()) null else m.group(2) + } + + fun sortMessagesByDate( + values: ArrayList, + firstOnTop: Boolean + ): ArrayList { + values.sortWith { m1, m2 -> + val d1 = m1.date + val d2 = m2.date + + if (firstOnTop) { + d2 - d1 + } else { + d1 - d2 + } + } + + return values + } + + fun sortConversationsByDate( + values: ArrayList, + firstOnTop: Boolean + ): ArrayList { + values.sortWith { c1, c2 -> + val d1 = c1.lastMessage.date + val d2 = c2.lastMessage.date + + return@sortWith if (firstOnTop) { + d2 - d1 + } else { + d1 - d2 + } + } + + return values + } + + fun matchMentions(text: String): String { + return text + } + +// fun removeTime(date: Date): Long { +// return Calendar.getInstance().apply { +// time = date +// this[Calendar.HOUR_OF_DAY] = 0 +// this[Calendar.MINUTE] = 0 +// this[Calendar.SECOND] = 0 +// this[Calendar.MILLISECOND] = 0 +// }.timeInMillis +// } + + fun getUserOnline(user: VKUser): String { + val r = AppGlobal.resources + return if (user.isOnline) { + if (user.isOnlineMobile) { + r.getString(R.string.user_online_mobile) + } else { + r.getString(R.string.user_online) + } + } else { + if (user.lastSeen == 0) { + r.getString(R.string.user_last_seen_recently) + } else { + r.getString(R.string.user_last_seen_at, getLastSeenTime(user.lastSeen * 1000L)) + } + } + } + + fun getUserOnlineIcon( + context: Context, + conversation: VKConversation?, + peerUser: VKUser? + ): Drawable? { + return if (conversation != null) { + if (conversation.isUser() && peerUser != null) { + if (!peerUser.isOnline) { + null + } else { + ContextCompat.getDrawable( + context, + if (peerUser.isOnlineMobile) R.drawable.ic_online_mobile else R.drawable.ic_online_pc + ) + } + } else null + } else { + if (peerUser!!.isOnline) { + ContextCompat.getDrawable( + context, + if (peerUser.isOnlineMobile) R.drawable.ic_online_mobile else R.drawable.ic_online_pc + ) + } else { + null + } + } + } + + fun getUserOnlineIcon(context: Context, user: VKUser): Drawable? { + return getUserOnlineIcon(context, null, user) + } + + //TODO: нормальное время + fun getLastSeenTime(date: Long): String { + return SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) + } + + fun getAvatarPlaceholder(context: Context, dialogTitle: String): TextDrawable { + return TextDrawable.builder().buildRound( + if (dialogTitle.isEmpty()) "" else { + TextUtils.getFirstLetterFromString(dialogTitle) + }, + context.color(R.color.accent) + ) + } + + @WorkerThread + fun searchUser(id: Int, onResponseListener: OnResponseListener? = null): VKUser? { + return if (VKGroup.isGroupId(id) || isChatId(id)) { + null + } else { + MemoryCache.getUserById(id)?.let { return it } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "User with id $id not found") + } + + TaskManager.loadUser(VKApiKeys.UPDATE_USER, id, onResponseListener) + + return null + } + } + + @WorkerThread + fun searchGroup(id: Int, onResponseListener: OnResponseListener? = null): VKGroup? { + return if (!VKGroup.isGroupId(id) || isChatId(id)) { + null + } else { + MemoryCache.getGroupById(abs(id))?.let { return it } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Group with id $id not found") + } + + TaskManager.loadGroup(VKApiKeys.UPDATE_GROUP, abs(id), onResponseListener) + + return null + } + } + + fun getTitle(conversation: VKConversation, peerUser: VKUser?, peerGroup: VKGroup?): String { + return when { + conversation.isUser() -> { + peerUser?.let { return it.toString() } ?: "" + } + + conversation.isGroup() -> { + peerGroup?.let { return it.name } ?: "" + } + + conversation.isChat() -> { + conversation.title + } + + else -> "" + } + } + + fun getMessageTitle(message: VKMessage, fromUser: VKUser?, fromGroup: VKGroup?): String { + return when { + message.isFromUser() -> { + fromUser?.let { return it.toString() } ?: "" + } + + message.isFromGroup() -> { + fromGroup?.let { return it.name } ?: "" + } + + else -> "" + } + } + + fun getAvatar(conversation: VKConversation, peerUser: VKUser?, peerGroup: VKGroup?): String { + return when { + conversation.isUser() -> { + peerUser?.let { return it.photo200 } ?: "" + } + + conversation.isGroup() -> { + peerGroup?.let { return it.photo200 } ?: "" + } + + conversation.isChat() -> { + conversation.photo200 + } + + else -> "" + } + } + + fun getUserAvatar(message: VKMessage, fromUser: VKUser?, fromGroup: VKGroup?): String { + return when { + message.isFromUser() -> { + fromUser?.let { return it.photo100 } ?: "" + } + + message.isFromGroup() -> { + fromGroup?.let { return it.photo100 } ?: "" + } + + else -> "" + } + } + + fun getUserPhoto(user: VKUser): String { + if (user.photo200.isEmpty()) { + if (user.photo100.isEmpty()) { + if (user.photo50.isEmpty()) { + return "" + } + } else { + return user.photo100 + } + } else { + return user.photo200 + } + + return "" + } + + fun getGroupPhoto(group: VKGroup): String { + if (group.photo200.isEmpty()) { + if (group.photo100.isEmpty()) { + if (group.photo50.isEmpty()) { + return "" + } + } else { + return group.photo100 + } + } else { + return group.photo200 + } + + return "" + } + + fun getDialogType(context: Context, conversation: VKConversation): Drawable? { + return when { + conversation.isGroupChannel -> { + ContextCompat.getDrawable(context, R.drawable.ic_dialog_type_channel) + } + conversation.isChat() -> { + ContextCompat.getDrawable(context, R.drawable.ic_dialog_type_conversation) + } + else -> null + } + } + + fun getAttachmentText(context: Context, attachments: List): String { + val resId: Int + + if (attachments.isNotEmpty()) { + if (attachments.size > 1) { + var oneType = true + val className = attachments[0].javaClass.simpleName + + for (model in attachments) { + if (model.javaClass.simpleName != className) { + oneType = false + break + } + } + + return if (oneType) { + val objectClass: Class = attachments[0].javaClass + resId = when (objectClass) { + VKPhoto::class.java -> { + R.string.message_attachment_photos + } + VKVideo::class.java -> { + R.string.message_attachment_videos + } + VKAudio::class.java -> { + R.string.message_attachment_audios + } + VKDoc::class.java -> { + R.string.message_attachment_docs + } + else -> { + -1 + } + } + if (resId == -1) "Unknown attachments" else context.getString( + resId, + attachments.size + ).toLowerCase(Locale.getDefault()) + } else { + context.getString(R.string.message_attachments_many) + } + } else { + val objectClass: Class = attachments[0].javaClass + + resId = when (objectClass) { + VKPhoto::class.java -> { + R.string.message_attachment_photo + } + VKAudio::class.java -> { + R.string.message_attachment_audio + } + VKVideo::class.java -> { + R.string.message_attachment_video + } + VKDoc::class.java -> { + R.string.message_attachment_doc + } + VKGraffiti::class.java -> { + R.string.message_attachment_graffiti + } + VKAudioMessage::class.java -> { + R.string.message_attachment_voice + } + VKSticker::class.java -> { + R.string.message_attachment_sticker + } + VKGift::class.java -> { + R.string.message_attachment_gift + } + VKLink::class.java -> { + R.string.message_attachment_link + } + VKPoll::class.java -> { + R.string.message_attachment_poll + } + VKCall::class.java -> { + R.string.message_attachment_call + } + else -> { + return "Unknown" + } + } + } + } else { + return "" + } + return context.getString(resId) + } + + fun getAttachmentDrawable(context: Context, attachments: List): Drawable? { + if (attachments.isEmpty() || attachments.size > 1) return null + + var resId = -1 + + when (attachments[0].javaClass) { + VKPhoto::class.java -> { + resId = R.drawable.ic_message_attachment_camera + } + VKAudio::class.java -> { + resId = R.drawable.ic_message_attachment_audio + } + VKVideo::class.java -> { + resId = R.drawable.ic_message_attachment_video + } + VKDoc::class.java -> { + resId = R.drawable.ic_message_attachment_doc + } + VKGraffiti::class.java -> { + resId = R.drawable.ic_message_attachment_graffiti + } + VKAudioMessage::class.java -> { + resId = R.drawable.ic_message_attachment_audio_message + } + VKSticker::class.java -> { + resId = R.drawable.ic_message_attachment_sticker + } + VKGift::class.java -> { + resId = R.drawable.ic_message_attachment_gift + } + VKLink::class.java -> { + resId = R.drawable.ic_message_attachment_link + } + VKPoll::class.java -> { + resId = R.drawable.ic_message_attachment_poll + } + VKCall::class.java -> { + resId = R.drawable.ic_message_attachment_call + } + } + + if (resId != -1) { + val drawable = context.drawable(resId) + + drawable?.setTint(context.color(R.color.accent)) + return drawable + } + return null + } + + fun getFwdText(context: Context, forwardedMessages: List): String { + return if (forwardedMessages.isNotEmpty()) { + if (forwardedMessages.size > 1) { + context.getString(R.string.message_fwd_many, forwardedMessages.size).lowerCase() + } else { + context.getString(R.string.message_fwd_one) + } + } else "" + } + + @Deprecated("need to rewrite") + fun getActionText( + context: Context, + lastMessage: VKMessage, + onResponseListener: OnResponseListener + ) { + TaskManager.execute { + lastMessage.action?.let { + var result = "" + + when (it.type) { + VKMessageAction.ACTION_CHAT_CREATE -> result = context.getString( + R.string.message_action_created_chat, + "" + ) + VKMessageAction.ACTION_INVITE_USER -> result = + if (lastMessage.fromId == lastMessage.action!!.memberId) { + context.getString(R.string.message_action_returned_to_chat, "") + } else { + val invited = MemoryCache.getUserById(lastMessage.action!!.memberId) + context.getString(R.string.message_action_invited_user, invited) + } + VKMessageAction.ACTION_INVITE_USER_BY_LINK -> result = context.getString( + R.string.message_action_invited_by_link, + "" + ) + VKMessageAction.ACTION_KICK_USER -> result = + if (lastMessage.fromId == lastMessage.action!!.memberId) { + context.getString(R.string.message_action_left_from_chat, "") + } else { + val kicked = MemoryCache.getUserById(lastMessage.action!!.memberId) + context.getString(R.string.message_action_kicked_user, kicked) + } + VKMessageAction.ACTION_PHOTO_REMOVE -> result = context.getString( + R.string.message_action_removed_photo, + "" + ) + VKMessageAction.ACTION_PHOTO_UPDATE -> result = context.getString( + R.string.message_action_updated_photo, + "" + ) + VKMessageAction.ACTION_PIN_MESSAGE -> result = context.getString( + R.string.message_action_pinned_message, + "" + ) + VKMessageAction.ACTION_UNPIN_MESSAGE -> result = context.getString( + R.string.message_action_unpinned_message, + "" + ) + VKMessageAction.ACTION_TITLE_UPDATE -> result = context.getString( + R.string.message_action_updated_title, + "" + ) + } + + AppGlobal.post { onResponseListener.onResponse(result) } + } + } + } + + fun getTime(context: Context, lastMessage: VKMessage): String { + val then = lastMessage.date * 1000L + val now = System.currentTimeMillis() + + val change = abs(now - then) + + val seconds = change / 1000 + + if (seconds == 0L) { + return context.getString(R.string.time_format_now) + } + + val minutes = seconds / 60 + + if (minutes == 0L) { + return context.getString(R.string.time_format_second, seconds) + } + + val hours = minutes / 60 + + if (hours == 0L) { + return context.getString(R.string.time_format_minute, minutes) + } + + val days = hours / 24 + + if (days == 0L) { + return context.getString(R.string.time_format_hour, hours) + } + + val months = days / 30 + + if (months == 0L) { + return context.getString(R.string.time_format_day, days) + } + + val years = months / 12 + + if (years == 0L) { + return context.getString(R.string.time_format_month, months) + } else if (years > 0L) { + return context.getString(R.string.time_format_year, years) + } + + return SimpleDateFormat("HH:mm", Locale.getDefault()).format(then) + } + + fun parseConversations(array: JSONArray): ArrayList { + val conversations = arrayListOf() + for (i in 0 until array.length()) { + conversations.add(VKConversation(array.optJSONObject(i))) + } + + return conversations + } + + fun parseMessages(array: JSONArray): ArrayList { + val messages = arrayListOf() + for (i in 0 until array.length()) { + messages.add(VKMessage(array.optJSONObject(i))) + } + + return messages + } + + fun isChatId(id: Int) = id > 2_000_000_000 + + fun isMessageHasFlag(mask: Int, flagName: String): Boolean { + val o: Any? = VKMessage.flags[flagName] + return if (o != null) { //has flag + val flag = o as Int + flag and mask > 0 + } else false + } + + //TODO: rewrite parsing + //fromUser and fromGroup are null + @Deprecated("need to rewrite") + @WorkerThread + fun parseLongPollMessage(array: JSONArray): VKMessage { + val message = VKMessage() + + val id = array.optInt(1) + val flags = array.optInt(2) + val peerId = array.optInt(3) + val date = array.optInt(4) + val text = array.optString(5) + + message.messageId = id + message.peerId = peerId + message.date = date + message.text = text + + val fromId = + if (isMessageHasFlag(flags, "outbox")) UserConfig.userId + else peerId + + message.fromId = fromId + + array.optJSONObject(6)?.let { + if (it.has("emoji")) message.hasEmoji = true + + if (it.has("from")) { + message.fromId = it.optInt("from", -1) + } + + if (it.has("source_act")) { + message.action = VKMessageAction().also { action -> + action.type = it.optString("source_act") + + when (action.type) { + VKMessageAction.ACTION_CHAT_CREATE -> { + action.text = it.optString("source_text") + } + VKMessageAction.ACTION_TITLE_UPDATE -> { + action.oldText = it.optString("source_old_text") + action.text = it.optString("source_text") + } + VKMessageAction.ACTION_PIN_MESSAGE -> { + action.memberId = it.optInt("source_mid") + action.conversationMessageId = it.optInt("source_chat_local_id") + + it.optJSONObject("source_message")?.let { message -> + action.message = VKMessage(message) + } + } + VKMessageAction.ACTION_UNPIN_MESSAGE -> { + action.memberId = it.optInt("source_mid") + action.conversationMessageId = it.optInt("source_chat_local_id") + } + VKMessageAction.ACTION_INVITE_USER, + VKMessageAction.ACTION_KICK_USER, + VKMessageAction.ACTION_SCREENSHOT, + VKMessageAction.ACTION_INVITE_USER_BY_CALL -> { + action.memberId = it.optInt("source_mid") + } + } + } + } + } + + array.optJSONObject(7)?.let { + /** + * + * fwd? reply? attachments_count? attachments? + * + */ + } + + val randomId = array.optInt(8) + message.randomId = randomId + + val conversationMessageId = array.optInt(9) + message.conversationMessageId = conversationMessageId + + val editTime = array.optInt(10) + message.editTime = editTime + + val out = fromId == UserConfig.userId + message.isOut = out + + if (message.isFromUser()) { + message.fromUser = MemoryCache.getUserById(fromId) + } else { + message.fromGroup = MemoryCache.getGroupById(abs(fromId)) + } + + return message + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt new file mode 100644 index 00000000..e0aa3937 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseActivity.kt @@ -0,0 +1,12 @@ +package ru.melod1n.project.vkm.base + +import android.view.View +import androidx.appcompat.app.AppCompatActivity + +abstract class BaseActivity : AppCompatActivity() { + + fun getRootView(): View { + return findViewById(android.R.id.content) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt new file mode 100644 index 00000000..55cc2b89 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseAdapter.kt @@ -0,0 +1,175 @@ +package ru.melod1n.project.vkm.base + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import androidx.recyclerview.widget.RecyclerView +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList +import ru.melod1n.project.vkm.listener.ItemClickListener +import ru.melod1n.project.vkm.listener.ItemLongClickListener +import java.io.Serializable +import java.util.* + + +@Suppress("UNCHECKED_CAST") +abstract class BaseAdapter( + var context: Context, + var values: ArrayList = arrayListOf() +) : RecyclerView.Adapter() { + + companion object { + private const val P_ITEMS = "BaseAdapter.values" + } + + private var cleanValues: ArrayList? = null + + private var inflater: LayoutInflater = LayoutInflater.from(context) + + var itemClickListener: ItemClickListener? = null + var itemLongClickListener: ItemLongClickListener? = null + + open fun destroy() {} + + open fun getItem(position: Int): T { + return values[position] + } + + fun add(position: Int, item: T) { + values.add(position, item) + cleanValues?.add(position, item) + } + + fun add(item: T) { + values.add(item) + cleanValues?.add(item) + } + + fun addAll(items: List) { + values.addAll(items) + cleanValues?.addAll(items) + } + + fun addAll(position: Int, items: List) { + values.addAll(position, items) + cleanValues?.addAll(position, items) + } + + operator fun set(position: Int, item: T) { + values[position] = item + cleanValues?.set(position, item) + } + + fun indexOf(item: T): Int { + return values.indexOf(item) + } + + fun removeAt(index: Int) { + values.removeAt(index) + cleanValues?.removeAt(index) + } + + fun remove(item: T) { + values.remove(item) + cleanValues?.remove(item) + } + + fun isEmpty() = values.isNullOrEmpty() + + fun isNotEmpty() = !isEmpty() + + fun view(resId: Int, viewGroup: ViewGroup): View { + return inflater.inflate(resId, viewGroup, false) + } + + fun updateValues(arrayList: ArrayList) { + values.clear() + values.addAll(arrayList) + } + + fun updateValues(list: List) = updateValues(list.asArrayList()) + + override fun onBindViewHolder(holder: VH, position: Int) { + onBindItemViewHolder(holder, position) + } + + protected fun initListeners(itemView: View, position: Int) { + if (itemView is AdapterView<*>) return + + itemView.setOnClickListener { + if (itemClickListener != null) itemClickListener!!.onItemClick( + position + ) + } + itemView.setOnLongClickListener { + if (itemLongClickListener != null) itemLongClickListener!!.onItemLongClick(position) + itemClickListener == null + } + } + + override fun getItemCount(): Int { + return values.size + } + + private fun onBindItemViewHolder(holder: VH, position: Int) { + initListeners(holder.itemView, position) + holder.bind(position) + } + + fun onSaveInstanceState(): Parcelable { + val bundle = Bundle() + if (values.size > 0 && (values[0] is Parcelable || values[0] is Serializable)) { + bundle.putSerializable(P_ITEMS, values) + } + return bundle + } + + fun post(runnable: Runnable) { + AppGlobal.handler.post(runnable) + } + + fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + if (state.containsKey(P_ITEMS)) { + values = state.getSerializable(P_ITEMS) as ArrayList + } + } + } + + fun clear() { + values.clear() + } + + open fun filter(query: String) { + if (cleanValues == null) { + cleanValues = ArrayList(values) + } + + values.clear() + + if (query.isEmpty()) { + values.addAll(cleanValues!!) + } else { + for (item in cleanValues!!) { + if (onQueryItem(item, query)) { + values.add(item) + } + } + } + + notifyDataSetChanged() + } + + open fun onQueryItem(item: T, query: String): Boolean { + return false + } + + operator fun get(index: Int): T { + return values[index] + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt new file mode 100644 index 00000000..5e5e8b7c --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFragment.kt @@ -0,0 +1,21 @@ +package ru.melod1n.project.vkm.base + +import androidx.annotation.IdRes +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import ru.melod1n.project.vkm.activity.MainActivity + +abstract class BaseFragment : Fragment() { + + protected open fun initToolbar(@IdRes resId: Int) { + val toolbar: Toolbar = requireView().findViewById(resId) + + activity?.let { + if (it is MainActivity && toolbar is ru.melod1n.project.vkm.widget.Toolbar) it.initToolbar(toolbar) + } + } + + protected fun runOnUi(runnable: Runnable) { + activity?.runOnUiThread(runnable) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt new file mode 100644 index 00000000..4728256c --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseFullscreenDialog.kt @@ -0,0 +1,33 @@ +package ru.melod1n.project.vkm.base + +import android.os.Bundle +import android.view.ViewGroup +import android.view.WindowManager +import androidx.fragment.app.DialogFragment +import ru.melod1n.project.vkm.R + +abstract class BaseFullscreenDialog : DialogFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setStyle(STYLE_NORMAL, R.style.AppTheme_FullScreenDialog) + } + + override fun onStart() { + super.onStart() + + dialog?.let { dialog -> + val width = ViewGroup.LayoutParams.MATCH_PARENT + val height = ViewGroup.LayoutParams.MATCH_PARENT + + dialog.window?.let { + it.setLayout(width, height) + it.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + it.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + + it.setWindowAnimations(R.style.AppTheme_Slide) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt b/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt new file mode 100644 index 00000000..c54a55b4 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/BaseHolder.kt @@ -0,0 +1,8 @@ +package ru.melod1n.project.vkm.base + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { + abstract fun bind(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt new file mode 100644 index 00000000..80574680 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpBase.kt @@ -0,0 +1,21 @@ +package ru.melod1n.project.vkm.base.mvp + +import android.app.Application +import android.os.Handler + +object MvpBase { + + lateinit var handler: Handler + + fun init(application: Application) { + handler = Handler(application.mainLooper) + } + + fun init(appHandler: Handler) { + handler = appHandler + } + + fun post(runnable: Runnable) { + handler.post(runnable) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt new file mode 100644 index 00000000..b3286ffe --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpConstants.kt @@ -0,0 +1,11 @@ +package ru.melod1n.project.vkm.base.mvp + +object MvpConstants { + + const val PEER_ID = "_peer_id" + const val ID = "_id" + const val COUNT = "_count" + const val OFFSET = "_offset" + const val FROM_CACHE = "_from_cache" + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt new file mode 100644 index 00000000..8cb241a1 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpException.kt @@ -0,0 +1,3 @@ +package ru.melod1n.project.vkm.base.mvp + +class MvpException(var errorId: String) : Exception(errorId) \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt new file mode 100644 index 00000000..42feda41 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpFields.kt @@ -0,0 +1,30 @@ +package ru.melod1n.project.vkm.base.mvp + +import androidx.collection.arrayMapOf +import java.util.* + +@Suppress("UNCHECKED_CAST") +class MvpFields { + private val fields = arrayMapOf() + + fun put(key: String, value: Any): MvpFields { + fields[key] = value + return this + } + + operator fun get(key: String): T { + return fields[key] as T + } + + fun getNonNull(key: String): Any { + return fields[key]!! + } + + fun getNonNull(`object`: Any): Any { + return Objects.requireNonNull(`object`) + } + + fun getFields(): Map { + return fields + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt new file mode 100644 index 00000000..82033808 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpOnLoadListener.kt @@ -0,0 +1,9 @@ +package ru.melod1n.project.vkm.base.mvp + +interface MvpOnLoadListener { + + fun onResponse(response: T) + + fun onError(t: Throwable) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt new file mode 100644 index 00000000..20905db4 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpPresenter.kt @@ -0,0 +1,106 @@ +package ru.melod1n.project.vkm.base.mvp + +import android.content.Context +import android.os.Bundle +import androidx.annotation.NonNull +import androidx.annotation.Nullable + +@Suppress("UNCHECKED_CAST") +abstract class MvpPresenter, V : MvpView>( + protected var viewState: V, + private val repositoryStringClassName: String +) { + + protected var context: Context? = null + + protected fun requireContext(): Context { + if (context == null) throw IllegalStateException("context is null") + + return context!! + } + + enum class ListState { + EMPTY, EMPTY_LOADING, EMPTY_NO_INTERNET, EMPTY_ERROR, FILLED, FILLED_LOADING + } + + protected var tag: String = "" + + lateinit var repository: Repository + + init { + initRepository() + } + + private fun initRepository() { + val clazz = Class.forName(repositoryStringClassName) + + this.repository = clazz.newInstance() as Repository + } + + open fun onCreate(context: Context, bundle: Bundle? = null) { + this.context = context + } + + open fun onCreateView(bundle: Bundle? = null) {} + open fun onViewCreated(bundle: Bundle? = null) {} + + protected fun post(runnable: Runnable) { + MvpBase.post(runnable) + } + + open fun destroy() {} + + fun prepareViews() { + viewState.prepareNoItemsView() + viewState.prepareNoInternetView() + viewState.prepareErrorView() + } + + fun setState(state: ListState) { + when (state) { + ListState.EMPTY -> { + viewState.hideRefreshLayout() + viewState.hideProgressBar() + viewState.showNoItemsView() + viewState.hideNoInternetView() + viewState.hideErrorView() + } + ListState.EMPTY_LOADING -> { + viewState.hideRefreshLayout() + viewState.showProgressBar() + viewState.hideNoItemsView() + viewState.hideNoInternetView() + viewState.hideErrorView() + } + ListState.EMPTY_NO_INTERNET -> { + viewState.hideRefreshLayout() + viewState.hideProgressBar() + viewState.hideNoItemsView() + viewState.showNoInternetView() + viewState.hideErrorView() + } + ListState.EMPTY_ERROR -> { + viewState.hideRefreshLayout() + viewState.hideProgressBar() + viewState.hideNoItemsView() + viewState.hideNoInternetView() + viewState.showErrorView() + } + ListState.FILLED -> { + viewState.hideRefreshLayout() + viewState.hideProgressBar() + viewState.hideNoItemsView() + viewState.hideNoInternetView() + viewState.hideErrorView() + } + ListState.FILLED_LOADING -> { + viewState.showRefreshLayout() + viewState.hideProgressBar() + viewState.hideNoItemsView() + viewState.hideNoInternetView() + viewState.hideErrorView() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt new file mode 100644 index 00000000..9b6dbeb9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpRepository.kt @@ -0,0 +1,48 @@ +package ru.melod1n.project.vkm.base.mvp + +@Suppress("UNCHECKED_CAST") +abstract class MvpRepository { + + // abstract fun loadValues(fields: MvpFields, listener: MvpOnLoadListener?) +// +// abstract fun loadCachedValues(fields: MvpFields, listener: MvpOnLoadListener?) +// +// protected fun sendError(listener: MvpOnLoadListener?, errorId: String) { +// listener?.onErrorLoad(MvpException(errorId)) +// } +// + protected fun sendError( + listener: MvpOnLoadListener, + t: Throwable + ) { +// if (listener !is MvpOnLoadListener) return + + MvpBase.post { listener.onError(t) } + } + + protected fun sendResponseArray( + listener: MvpOnLoadListener>, + response: ArrayList + ) { + listener.let { MvpBase.handler.post { listener.onResponse(response) } } + } + + protected fun sendResponse( + listener: MvpOnLoadListener, + response: Item + ) { + listener.let { + MvpBase.handler.post { listener.onResponse(response) } + } + } + +// protected open fun cacheLoadedValues(values: ArrayList) {} +// +// protected fun startNewThread(runnable: Runnable) { +// Thread(runnable).start() +// } +// +// protected fun post(runnable: Runnable) { +// MvpBase.handler.post(runnable) +// } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt new file mode 100644 index 00000000..485fb1cc --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/base/mvp/MvpView.kt @@ -0,0 +1,33 @@ +package ru.melod1n.project.vkm.base.mvp + +interface MvpView { + + fun showErrorSnackbar(t: Throwable) + + fun prepareNoItemsView() + + fun showNoItemsView() + + fun hideNoItemsView() + + fun prepareNoInternetView() + + fun showNoInternetView() + + fun hideNoInternetView() + + fun prepareErrorView() + + fun showErrorView() + + fun hideErrorView() + + fun showProgressBar() + + fun hideProgressBar() + + fun showRefreshLayout() + + fun hideRefreshLayout() + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt b/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt new file mode 100644 index 00000000..2bfd4f02 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/common/AppGlobal.kt @@ -0,0 +1,163 @@ +package ru.melod1n.project.vkm.common + +import android.annotation.SuppressLint +import android.app.Application +import android.app.DownloadManager +import android.content.ClipboardManager +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Resources +import android.net.ConnectivityManager +import android.os.Handler +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.pm.PackageInfoCompat +import androidx.preference.PreferenceManager +import androidx.room.Room +import com.facebook.drawee.backends.pipeline.Fresco +import com.microsoft.appcenter.AppCenter +import com.microsoft.appcenter.analytics.Analytics +import com.microsoft.appcenter.crashes.Crashes +import org.acra.ACRA +import org.acra.ReportingInteractionMode +import org.acra.annotation.ReportsCrashes +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.base.mvp.MvpBase +import ru.melod1n.project.vkm.database.AppDatabase +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.fragment.FragmentSettings +import ru.melod1n.project.vkm.util.AndroidUtils +import java.util.* + +@SuppressLint("NonConstantResourceId") +@ReportsCrashes( + mailTo = "lischenkodev@gmail.com", + mode = ReportingInteractionMode.DIALOG, + resDialogTitle = R.string.app_has_been_crashed, + resDialogText = R.string.empty, + resDialogTheme = R.style.AppTheme_Dialog, + resDialogPositiveButtonText = R.string.send_crash_report, + resDialogNegativeButtonText = R.string.ok +) +class AppGlobal : Application() { + + companion object { + const val APP_CENTER_TOKEN = "c87e410a-d622-4c52-ad7e-7388ab511704" + + lateinit var windowManager: WindowManager + lateinit var connectivityManager: ConnectivityManager + lateinit var inputMethodManager: InputMethodManager + lateinit var clipboardManager: ClipboardManager + lateinit var downloadManager: DownloadManager + + lateinit var preferences: SharedPreferences + lateinit var locale: Locale + lateinit var handler: Handler + lateinit var resources: Resources + lateinit var packageName: String + lateinit var database: AppDatabase + lateinit var instance: AppGlobal + + lateinit var packageManager: PackageManager + + var versionName = "" + var versionCode = 0L + + var screenWidth = 0 + var screenHeight = 0 + + fun post(runnable: Runnable) { + handler.post(runnable) + } + } + + override fun onCreate() { + super.onCreate() + instance = this + + if (!BuildConfig.DEBUG) { + AppCenter.start( + this, APP_CENTER_TOKEN, Analytics::class.java, Crashes::class.java + ) + + ACRA.init(this) + } + + Fresco.initialize(this) + + database = Room.databaseBuilder(this, AppDatabase::class.java, "cache") + .fallbackToDestructiveMigration() + .build() + + preferences = PreferenceManager.getDefaultSharedPreferences(this) + handler = Handler(mainLooper) + locale = Locale.getDefault() + + val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES) + versionName = info.versionName + versionCode = PackageInfoCompat.getLongVersionCode(info) + + Companion.resources = resources + Companion.packageName = packageName + Companion.packageManager = packageManager + + inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + + screenWidth = AndroidUtils.getDisplayWidth() + screenHeight = AndroidUtils.getDisplayHeight() + + UserConfig.restore() + + VKApi.init(this) + + MvpBase.init(handler) + + fillMemoryCache() + + applyNightMode() + } + + private fun fillMemoryCache() { + TaskManager.execute { + val users = database.users.getAll() + val groups = database.groups.getAll() + + MemoryCache.appendUsers(users) + MemoryCache.appendGroups(groups) + } + } + + fun applyNightMode(value: String? = null) { + val mode = value ?: preferences.getString(FragmentSettings.KEY_THEME, "-1")!! + + val nightMode = getNightMode(mode.toInt()) + + val oldNightMode = AppCompatDelegate.getDefaultNightMode() + + AppCompatDelegate.setDefaultNightMode(nightMode) + } + + fun getNightMode(nightMode: Int = -1): Int { + val mode = if (nightMode != -1) nightMode else preferences.getString( + FragmentSettings.KEY_THEME, + "-1" + )!!.toInt() + + return when (mode) { + 1 -> AppCompatDelegate.MODE_NIGHT_YES + 2 -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY + 3 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + else -> AppCompatDelegate.MODE_NIGHT_NO + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt b/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt new file mode 100644 index 00000000..de40b341 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/common/FragmentSwitcher.kt @@ -0,0 +1,118 @@ +package ru.melod1n.project.vkm.common + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.fragment.FragmentConversations +import ru.melod1n.project.vkm.fragment.FragmentFriends +import ru.melod1n.project.vkm.fragment.FragmentImportant + +object FragmentSwitcher { + + fun getCurrentFragment(fragmentManager: FragmentManager): Fragment? { + val fragments = fragmentManager.fragments + + if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty") + + for (fragment in fragments) { + if (fragment.isVisible) { + return fragment + } + } + + return null + } + + fun addFragments( + fragmentManager: FragmentManager, + containerId: Int, + fragments: Collection + ) { + val transaction = fragmentManager.beginTransaction() + + for (fragment in fragments) { + transaction.add(containerId, fragment, fragment.javaClass.simpleName) + } + + transaction.commitNow() + } + + fun showFragment(fragmentManager: FragmentManager, tag: String) { + showFragment(fragmentManager, tag, false) + } + + fun showFragment( + fragmentManager: FragmentManager, + tag: String, + hideOthers: Boolean, + containerId: Int = R.id.fragmentContainer + ) { + val fragments = fragmentManager.fragments + + if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty") + + var fragmentToShow: Fragment? = null + + for (fragment in fragments) { + if (fragment.tag != null && fragment.tag == tag) { + fragmentToShow = fragment + break + } + } + + val transaction = fragmentManager.beginTransaction() + + if (fragmentToShow == null) { + fragmentToShow = createFragmentByTag(tag) + transaction.add(containerId, fragmentToShow, tag) + } else { + transaction.show(fragmentToShow) + } + + if (hideOthers) { + for (fragment in fragments) { + if (fragment.tag != null && fragment.tag == tag) continue + transaction.hide(fragment) + } + } + + transaction.commit() + } + + fun clearFragments(fragmentManager: FragmentManager) { + val fragments = fragmentManager.fragments + + if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty") + + val transaction = fragmentManager.beginTransaction() + + for (fragment in fragments) { + transaction.remove(fragment) + } + + transaction.commitNow() + } + + fun hideFragments(fragmentManager: FragmentManager) { + val fragments = fragmentManager.fragments + + if (fragments.isEmpty()) throw RuntimeException("FragmentManager's fragments is empty") + + val transaction = fragmentManager.beginTransaction() + + for (fragment in fragments) { + transaction.hide(fragment) + } + + transaction.commitNow() + } + + private fun createFragmentByTag(tag: String): Fragment { + return when (tag) { + "FragmentFriends" -> FragmentFriends() + "FragmentImportant" -> FragmentImportant() + "FragmentConversations" -> FragmentConversations() + else -> Fragment() + } + } +} diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt new file mode 100644 index 00000000..d18a4fc2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/common/TaskManager.kt @@ -0,0 +1,363 @@ +package ru.melod1n.project.vkm.common + +import android.util.Log +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.api.method.MethodSetter +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKGroup +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.concurrent.LowThread +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.listener.OnResponseListener +import java.util.* +import java.util.stream.Collectors + +object TaskManager { + + private const val TAG = "TaskManager" + + private val groupsTasksIds = arrayListOf() + + private var groupsTimer: Timer? = null + + private val usersTasksIds = arrayListOf() + + private var usersTimer: Timer? = null + + private val messagesTasksIds = arrayListOf() + + private val messagesReadIds = arrayListOf() + + private var messagesReadTimer: Timer? = null + + private val conversationsTasksIds = arrayListOf() + + private val listeners = arrayListOf() + + fun addOnEventListener(listener: OnEventListener?) { + listeners.add(listener) + } + + fun removeOnEventListener(listener: OnEventListener?) { + listeners.remove(listener) + } + + fun execute(runnable: Runnable?) { + LowThread(runnable).start() + } + + private fun addProcedure( + methodSetter: MethodSetter, + className: Class, + pushInfo: EventInfo<*>?, + responseListener: OnResponseListener? + ) { + execute { + methodSetter.executeArray(className, object : OnResponseListener> { + override fun onResponse(response: ArrayList) { + if (response.isEmpty()) return + + responseListener?.onResponse(response[0]) + + pushInfo?.let { sendEvent(it) } + } + + override fun onError(t: Throwable) { + responseListener?.onError(t) + } + + }) + } + } + + fun sendEvent(eventInfo: EventInfo<*>) { + AppGlobal.handler.post { + for (listener in listeners) { + listener?.onNewEvent(eventInfo) + } + } + } + + fun loadUser( + eventKey: VKApiKeys, + userId: Int, + responseListener: OnResponseListener? = null + ) { + if (usersTasksIds.contains(userId)) return + usersTasksIds.add(userId) + + if (BuildConfig.DEBUG) { + Log.i(TAG, "Load user: $userId") + } + + if (usersTimer != null) { + usersTimer?.cancel() + } + + usersTimer = Timer() + usersTimer?.schedule(object : TimerTask() { + override fun run() { + val setter = VKApi.users() + .get() + .userIds(usersTasksIds) + .fields(VKUser.DEFAULT_FIELDS) + + val usersIds = arrayListOf() + usersTasksIds.forEach { usersIds.add(it.toString()) } + + addProcedure( + setter, + VKUser::class.java, + EventInfo(eventKey, usersTasksIds), + object : OnResponseListener { + override fun onResponse(response: VKUser) { + Log.d( + TAG, + "Loaded users: ${ + usersIds.stream().collect(Collectors.joining(", ")) + }" + ) + + usersTasksIds.remove(userId) + responseListener?.onResponse(response) + + execute { MemoryCache.put(response) } + } + + override fun onError(t: Throwable) { + Log.w( + TAG, + "Loaded users: ${ + usersIds.stream().collect(Collectors.joining(", ")) + }\nStack:${Log.getStackTraceString(t)}" + ) + responseListener?.onError(t) + } + + }) + + usersTimer = null + } + }, 500) + } + + fun loadGroup( + eventKey: VKApiKeys, + groupId: Int, + responseListener: OnResponseListener? = null + ) { + if (groupsTasksIds.contains(groupId)) return + groupsTasksIds.add(groupId) + + if (BuildConfig.DEBUG) { + Log.i(TAG, "Load group: $groupId") + } + + val setter = VKApi.groups().getById() + .groupIds(groupsTasksIds) + .fields(VKGroup.DEFAULT_FIELDS) + + if (groupsTimer != null) { + groupsTimer?.cancel() + } + + groupsTimer = Timer() + groupsTimer?.schedule(object : TimerTask() { + override fun run() { + val groupsIds = arrayListOf() + groupsTasksIds.forEach { groupsIds.add(it.toString()) } + + addProcedure( + setter, + VKGroup::class.java, + EventInfo(eventKey, groupsTasksIds), + object : OnResponseListener { + override fun onResponse(response: VKGroup) { + Log.d( + TAG, + "Loaded groups: ${ + groupsIds.stream().collect(Collectors.joining(", ")) + }" + ) + + groupsTasksIds.remove(groupId) + responseListener?.onResponse(response) + + execute { MemoryCache.put(response) } + } + + override fun onError(t: Throwable) { + Log.w( + TAG, + "Not loaded Group: ${ + groupsIds.stream().collect(Collectors.joining(", ")) + }\nStack: " + Log.getStackTraceString( + t + ) + ) + responseListener?.onError(t) + } + }) + } + }, 500) + + } + + fun loadMessage( + eventKey: VKApiKeys, + messageId: Int, + responseListener: OnResponseListener? = null + ) { + if (BuildConfig.DEBUG) { + Log.i(TAG, "Load message: $messageId") + } + if (messagesTasksIds.contains(messageId)) return + messagesTasksIds.add(messageId) + + val setter = VKApi.messages().getById() + .messageIds(messageId) + .extended(true) + .filter(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS) + + addProcedure( + setter, + VKMessage::class.java, + EventInfo(eventKey, messageId), + object : OnResponseListener { + override fun onResponse(response: VKMessage) { + Log.d(TAG, "Loaded message: $messageId") + + messagesTasksIds.remove(messageId) + responseListener?.onResponse(response) + + execute { MemoryCache.put(response) } + } + + override fun onError(t: Throwable) { + Log.w( + TAG, + "Not loaded message: $messageId. Stack: " + Log.getStackTraceString(t) + ) + responseListener?.onError(t) + } + }) + } + + fun readMessage( + eventKey: VKApiKeys, + peerId: Int, + messageId: Int, + responseListener: OnResponseListener? = null + ) { + if (messagesReadIds.contains(messageId)) return + messagesReadIds.add(messageId) + + if (BuildConfig.DEBUG) { + Log.i(TAG, "Read message: $messageId") + } + + if (messagesReadTimer != null) { + messagesReadTimer?.cancel() + } + + messagesReadTimer = Timer() + messagesReadTimer?.schedule(object : TimerTask() { + override fun run() { + val messagesIds = arrayListOf() + messagesReadIds.forEach { messagesIds.add(it.toString()) } + + val setter = VKApi.messages().markAsRead() +// .startMessageId(messageId) + .markConversationAsRead(true) + .peerId(peerId) + + addProcedure( + setter, + Int::class.java, + EventInfo(eventKey, arrayOf(peerId, messageId)), + object : OnResponseListener { + override fun onResponse(response: Int) { + Log.d( + TAG, + "Readed messages: ${ + messagesIds.stream().collect(Collectors.joining(", ")) + }" + ) + + messagesReadIds.remove(messageId) + responseListener?.onResponse(response) + + //TODO: update readed messages in cache +// execute { MemoryCache.put(response) } + } + + override fun onError(t: Throwable) { + Log.w( + TAG, + "Not readed messages: ${ + messagesIds.stream().collect(Collectors.joining(", ")) + }\nStack: " + Log.getStackTraceString( + t + ) + ) + responseListener?.onError(t) + } + } + ) + } + + }, 500) + } + + fun loadConversation( + eventKey: VKApiKeys, + conversationId: Int, + responseListener: OnResponseListener? = null + ) { + if (BuildConfig.DEBUG) { + Log.i(TAG, "Load conversation: $conversationId") + } + if (conversationsTasksIds.contains(conversationId)) return + conversationsTasksIds.add(conversationId) + + val setter = VKApi.messages() + .getConversationsById() + .peerIds(conversationId) + .extended(true) + .fields(VKUser.DEFAULT_FIELDS + "," + VKGroup.DEFAULT_FIELDS) + + addProcedure( + setter, + VKConversation::class.java, + EventInfo(eventKey, conversationId), + object : OnResponseListener { + override fun onResponse(response: VKConversation) { + Log.d(TAG, "Loaded conversation: $conversationId") + + conversationsTasksIds.remove(conversationId) + responseListener?.onResponse(response) + + execute { MemoryCache.put(response) } + } + + override fun onError(t: Throwable) { + Log.w( + TAG, + "Not loaded conversation: $conversationId. Stack: " + Log.getStackTraceString( + t + ) + ) + responseListener?.onError(t) + } + + }) + } + + interface OnEventListener { + fun onNewEvent(info: EventInfo<*>) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt new file mode 100644 index 00000000..a782d901 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/common/TimeManager.kt @@ -0,0 +1,98 @@ +package ru.melod1n.project.vkm.common + +import android.content.Context +import android.content.IntentFilter +import ru.melod1n.project.vkm.receiver.MinuteReceiver +import java.util.* + +object TimeManager { + + var currentHour = 0 + var currentMinute = 0 + var currentSecond = 0 + + private val onHourChangeListeners: ArrayList = ArrayList() + private val onMinuteChangeListeners: ArrayList = ArrayList() + private val onSecondChangeListeners: ArrayList = ArrayList() + private val onTimeChangeListeners: ArrayList = ArrayList() + + fun init(context: Context) { + context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK")) + + addOnMinuteChangeListener(minuteChangeListener) + } + + private var minuteChangeListener = object : OnMinuteChangeListener { + override fun onMinuteChange(currentMinute: Int) { + TimeManager.currentMinute = currentMinute + } + } + + fun destroy() { + removeOnMinuteChangeListener(minuteChangeListener) + } + + fun broadcastMinute() { + for (onMinuteChangeListener in onMinuteChangeListeners) { + onMinuteChangeListener.onMinuteChange(0) + } + } + + val isMorning = currentHour in 7..11 + + val isAfternoon = currentHour in 12..16 + + val isEvening = currentHour in 17..22 + + val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1 + + fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) { + TimeManager.onHourChangeListeners.add(onHourChangeListeners) + } + + fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) { + onHourChangeListeners.remove(onHourChangeListener) + } + + fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) { + onMinuteChangeListeners.add(onMinuteChangeListener) + } + + fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) { + onMinuteChangeListeners.remove(onMinuteChangeListener) + } + + fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) { + onSecondChangeListeners.add(onSecondChangeListener) + } + + fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) { + onSecondChangeListeners.remove(onSecondChangeListener) + } + + fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) { + onTimeChangeListeners.add(onTimeChangeListener) + } + + fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) { + onTimeChangeListeners.remove(onTimeChangeListener) + } + + interface OnHourChangeListener { + fun onHourChange(currentHour: Int) + } + + interface OnMinuteChangeListener { + fun onMinuteChange(currentMinute: Int) + } + + interface OnSecondChangeListener { + fun onSecondChange(currentSecond: Int) + } + + interface OnTimeChangeListener { + fun onHourChange(currentHour: Int) + fun onMinuteChange(currentMinute: Int) + fun onSecondChange(currentSecond: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt b/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt new file mode 100644 index 00000000..69f63008 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/common/UpdateManager.kt @@ -0,0 +1,80 @@ +package ru.melod1n.project.vkm.common + +import android.util.Log +import androidx.collection.arrayMapOf +import org.json.JSONArray +import org.json.JSONObject +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.model.NewUpdateInfo +import ru.melod1n.project.vkm.net.HttpRequest + +object UpdateManager { + + interface OnUpdateListener { + fun onNewUpdate(updateInfo: NewUpdateInfo) + + fun onNoUpdates() + } + + private const val checkLink = "https://melodev.procsec.top/vkm/project_vkm_ota.json" + + private const val PRODUCT_NAME = "project_vkm" + private const val BRANCH = "alpha" + private const val OFFSET = 0 + + private const val TAG = "UpdateManager" + + fun checkUpdates(onUpdateListener: OnUpdateListener) { + TaskManager.execute { + val newLink = "https://temply.procsec.top/prop/deploy/api/method/getOTA" + + val params = arrayMapOf() + params["product"] = PRODUCT_NAME + params["branch"] = BRANCH + params["offset"] = OFFSET.toString() + params["code"] = AppGlobal.versionCode.toString() + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Request started") + } + + HttpRequest[newLink, params].asString().let { + AppGlobal.post { + if (BuildConfig.DEBUG) { + Log.d(TAG, "response: $it") + } + + val response: Any = if (it == "[]") JSONArray(it) else JSONObject(it) + + val newUpdateInfo: NewUpdateInfo? = + if (response is JSONArray) null else NewUpdateInfo(response as JSONObject) + + if (response is JSONArray || newUpdateInfo?.version?.isEmpty() == true || newUpdateInfo?.version == AppGlobal.versionName) { + onUpdateListener.onNoUpdates() + return@post + } else { + newUpdateInfo?.let { onUpdateListener.onNewUpdate(it) } + } + } + } + +// HttpRequest[checkLink].asString().let { +// val response = JSONObject(it) +// +// val updateInfo = UpdateInfo(response) +// +// AppGlobal.handler.post { +// if (updateInfo.version.isEmpty() || updateInfo.version == AppGlobal.versionName) { +// onUpdateListener.onNoUpdates() +// return@post +// } +// +// if (AppGlobal.versionName != updateInfo.version) { +// onUpdateListener.onNewUpdate(updateInfo) +// } +// } +// } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt b/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt new file mode 100644 index 00000000..77a5b4f3 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/concurrent/LowThread.kt @@ -0,0 +1,12 @@ +package ru.melod1n.project.vkm.concurrent + +import android.os.Process + +class LowThread(runnable: Runnable?) : Thread(runnable) { + + override fun run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) + super.run() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt b/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt new file mode 100644 index 00000000..8194fc4d --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/AppDatabase.kt @@ -0,0 +1,20 @@ +package ru.melod1n.project.vkm.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import ru.melod1n.project.vkm.api.model.* +import ru.melod1n.project.vkm.database.dao.* + +@Database( + entities = [VKConversation::class, VKMessage::class, VKUser::class, VKGroup::class, VKFriend::class], + version = 1, + exportSchema = false +) + +abstract class AppDatabase : RoomDatabase() { + abstract val conversations: ConversationsDao + abstract val messages: MessagesDao + abstract val users: UsersDao + abstract val groups: GroupsDao + abstract val friends: FriendsDao +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt b/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt new file mode 100644 index 00000000..08bb3ebc --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/MemoryCache.kt @@ -0,0 +1,176 @@ +package ru.melod1n.project.vkm.database + +import android.util.SparseArray +import androidx.annotation.WorkerThread +import ru.melod1n.project.vkm.api.model.* +import ru.melod1n.project.vkm.common.AppGlobal + +object MemoryCache { + + private val users = SparseArray() + private val groups = SparseArray() + + @WorkerThread + fun getUserById(id: Int): VKUser? { + var user = users[id] + if (user == null) { + user = AppGlobal.database.users.getById(id) + + user?.let { append(it) } + } + return user + } + + @WorkerThread + fun getGroupById(positiveId: Int): VKGroup? { + var group = groups[positiveId] + if (group == null) { + group = AppGlobal.database.groups.getById(positiveId) + + group?.let { append(it) } + } + return group + } + + @WorkerThread + fun getMessageById(id: Int): VKMessage? { + return AppGlobal.database.messages.getById(id) + } + + @WorkerThread + fun getMessagesByPeerId(peerId: Int): List { + return AppGlobal.database.messages.getByPeerId(peerId) + } + + @WorkerThread + fun getMessages(): List { + return AppGlobal.database.messages.getAll() + } + + @WorkerThread + fun getConversationById(id: Int): VKConversation? { + return AppGlobal.database.conversations.getById(id) + } + + @WorkerThread + fun getConversations(): List { + return AppGlobal.database.conversations.getAll() + } + + @WorkerThread + fun getFriends(userId: Int): List { + return AppGlobal.database.friends.getByUserId(userId) + } + + fun appendUsers(users: Collection) { + for (user in users) { + append(user) + } + } + + fun appendGroups(groups: Collection) { + for (group in groups) { + append(group) + } + } + + fun append(value: VKGroup) { + groups.append(value.groupId, value) + } + + fun append(value: VKUser) { + users.append(value.userId, value) + } + + @WorkerThread + fun put(value: VKUser) { + append(value) + + AppGlobal.database.users.insert(value) + } + + @WorkerThread + fun putUsers(users: List) { + appendUsers(users) + + AppGlobal.database.users.insert(users) + } + + @WorkerThread + fun put(value: VKFriend) { + AppGlobal.database.friends.insert(value) + } + + @WorkerThread + fun putFriends(friends: List) { + AppGlobal.database.friends.insert(friends) + } + + @WorkerThread + fun put(value: VKMessage) { + AppGlobal.database.messages.insert(value) + } + + @WorkerThread + fun putMessages(messages: List) { + AppGlobal.database.messages.insert(messages) + } + + @WorkerThread + fun put(value: VKGroup) { + append(value) + + AppGlobal.database.groups.insert(value) + } + + @WorkerThread + fun putGroups(groups: List) { + appendGroups(groups) + + AppGlobal.database.groups.insert(groups) + } + + @WorkerThread + fun put(value: VKConversation) { + AppGlobal.database.conversations.insert(value) + } + + @WorkerThread + fun putConversations(conversations: List) { + AppGlobal.database.conversations.insert(conversations) + } + + @WorkerThread + fun deleteMessage(messageId: Int, safe: Boolean = true) { + if (safe) { + AppGlobal.database.messages.getById(messageId) ?: return + } + + AppGlobal.database.messages.deleteById(messageId) + } + + @WorkerThread + fun deleteConversation(conversationId: Int, safe: Boolean = true) { + if (safe) { + AppGlobal.database.conversations.getById(conversationId) ?: return + } + + AppGlobal.database.conversations.deleteById(conversationId) + } + + @WorkerThread + fun edit(message: VKMessage?, safe: Boolean = true) { + message ?: return + + if (safe) { + AppGlobal.database.messages.getById(message.messageId) ?: return + } + + AppGlobal.database.messages.update(message) + } + + fun clear() { + users.clear() + groups.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt new file mode 100644 index 00000000..c46bfaba --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/ConversationsDao.kt @@ -0,0 +1,38 @@ +package ru.melod1n.project.vkm.database.dao + +import androidx.room.* +import ru.melod1n.project.vkm.api.model.VKConversation + +@Dao +interface ConversationsDao { + + @Query("SELECT * FROM conversations") + fun getAll(): List + + @Query("SELECT * FROM conversations WHERE conversationId = :id") + fun getById(id: Int): VKConversation? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: VKConversation) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Update + fun update(item: VKConversation) + + @Update + fun update(items: List) + + @Delete + fun delete(item: VKConversation) + + @Delete + fun delete(items: List) + + @Query("DELETE FROM conversations WHERE conversationId = :id") + fun deleteById(id: Int) + + @Query("DELETE FROM conversations") + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt new file mode 100644 index 00000000..15cdbe1c --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/FriendsDao.kt @@ -0,0 +1,41 @@ +package ru.melod1n.project.vkm.database.dao + +import androidx.room.* +import ru.melod1n.project.vkm.api.model.VKFriend + +@Dao +interface FriendsDao { + + @Query("SELECT * FROM friends") + fun getAll(): List + + @Query("SELECT * FROM friends WHERE userId = :id") + fun getById(id: Int): VKFriend? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: VKFriend) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Update + fun update(item: VKFriend) + + @Update + fun update(items: List) + + @Delete + fun delete(item: VKFriend) + + @Delete + fun delete(items: List) + + @Query("DELETE FROM friends WHERE userId = :id") + fun deleteById(id: Int) + + @Query("DELETE FROM friends") + fun clear() + + @Query("SELECT * FROM friends WHERE userId = :id") + fun getByUserId(id: Int): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt new file mode 100644 index 00000000..539d1d1e --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/GroupsDao.kt @@ -0,0 +1,37 @@ +package ru.melod1n.project.vkm.database.dao + +import androidx.room.* +import ru.melod1n.project.vkm.api.model.VKGroup + +@Dao +interface GroupsDao { + @Query("SELECT * FROM groups") + fun getAll(): List + + @Query("SELECT * FROM groups WHERE groupId = :id") + fun getById(id: Int): VKGroup? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: VKGroup) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Update + fun update(item: VKGroup) + + @Update + fun update(items: List) + + @Delete + fun delete(item: VKGroup) + + @Delete + fun delete(items: List) + + @Query("DELETE FROM groups WHERE groupId = :id") + fun deleteById(id: Int) + + @Query("DELETE FROM groups") + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt new file mode 100644 index 00000000..9e3e68b9 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/MessagesDao.kt @@ -0,0 +1,41 @@ +package ru.melod1n.project.vkm.database.dao + +import androidx.room.* +import ru.melod1n.project.vkm.api.model.VKMessage + +@Dao +interface MessagesDao { + + @Query("SELECT * FROM messages") + fun getAll(): List + + @Query("SELECT * FROM messages WHERE messageId = :id") + fun getById(id: Int): VKMessage? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: VKMessage) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Update + fun update(item: VKMessage) + + @Update + fun update(items: List) + + @Delete + fun delete(item: VKMessage) + + @Delete + fun delete(items: List) + + @Query("DELETE FROM messages WHERE messageId = :id") + fun deleteById(id: Int) + + @Query("DELETE FROM messages") + fun clear() + + @Query("SELECT * FROM messages WHERE peerId = :peerId") + fun getByPeerId(peerId: Int): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt new file mode 100644 index 00000000..15ad7537 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/UsersDao.kt @@ -0,0 +1,38 @@ +package ru.melod1n.project.vkm.database.dao + +import androidx.room.* +import ru.melod1n.project.vkm.api.model.VKUser + +@Dao +interface UsersDao { + + @Query("SELECT * FROM users") + fun getAll(): List + + @Query("SELECT * FROM users WHERE userId = :id") + fun getById(id: Int): VKUser? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: VKUser) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List) + + @Update + fun update(item: VKUser) + + @Update + fun update(items: List) + + @Delete + fun delete(item: VKUser) + + @Delete + fun delete(items: List) + + @Query("DELETE FROM users WHERE userId = :id") + fun deleteById(id: Int) + + @Query("DELETE FROM users") + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ArrayListToByteArrayConverter.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ArrayListToByteArrayConverter.kt new file mode 100644 index 00000000..021a59db --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ArrayListToByteArrayConverter.kt @@ -0,0 +1,24 @@ +package ru.melod1n.project.vkm.database.dao.converters + +import androidx.room.TypeConverter +import ru.melod1n.project.vkm.api.model.VKModel +import ru.melod1n.project.vkm.extensions.ArrayExtensions.isNullOrEmpty +import ru.melod1n.project.vkm.util.Utils + +@Suppress("UNCHECKED_CAST") +class ArrayListToByteArrayConverter { + + @TypeConverter + fun toForwarded(data: ByteArray?): ArrayList { + return if (data.isNullOrEmpty()) arrayListOf() else { + val deserializedData = Utils.deserialize(data) + if (deserializedData == null) arrayListOf() else deserializedData as ArrayList + } + } + + @TypeConverter + fun fromForwarded(forwarded: List): ByteArray { + return Utils.serialize(forwarded) ?: return byteArrayOf() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ForwardedConverter.kt b/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ForwardedConverter.kt new file mode 100644 index 00000000..0f8b78cc --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/database/dao/converters/ForwardedConverter.kt @@ -0,0 +1,25 @@ +package ru.melod1n.project.vkm.database.dao.converters + +import androidx.room.TypeConverter +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.extensions.ArrayExtensions.isNullOrEmpty +import ru.melod1n.project.vkm.util.Utils +import java.util.* + +@Suppress("UNCHECKED_CAST") +class ForwardedConverter { + + @TypeConverter + fun toForwarded(data: ByteArray?): ArrayList { + return if (data.isNullOrEmpty()) arrayListOf() else { + val deserializedData = Utils.deserialize(data) + if (deserializedData == null) arrayListOf() else deserializedData as ArrayList + } + } + + @TypeConverter + fun fromForwarded(forwarded: List): ByteArray { + return Utils.serialize(forwarded) ?: return byteArrayOf() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/dialog/AccountDialog.kt b/app/src/main/java/ru/melod1n/project/vkm/dialog/AccountDialog.kt new file mode 100644 index 00000000..e68bb207 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/dialog/AccountDialog.kt @@ -0,0 +1,115 @@ +package ru.melod1n.project.vkm.dialog + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.SettingsActivity +import ru.melod1n.project.vkm.adapter.SimpleItemAdapter +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.base.BaseFullscreenDialog +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById +import ru.melod1n.project.vkm.item.SimpleMenuItem +import ru.melod1n.project.vkm.listener.ItemClickListener +import ru.melod1n.project.vkm.util.ViewUtils +import ru.melod1n.project.vkm.widget.Toolbar + +class AccountDialog : BaseFullscreenDialog(), ItemClickListener { + + companion object { + const val TAG = "account_fullscreen_dialog" + } + + private lateinit var adapter: SimpleItemAdapter + + private lateinit var toolbar: Toolbar + private lateinit var recyclerView: RecyclerView + private lateinit var refreshLayout: SwipeRefreshLayout + private lateinit var headerRoot: RelativeLayout + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_account, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initViews() + prepareToolbar() + prepareRecyclerView() + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + recyclerView = findViewById(R.id.recyclerView) + refreshLayout = findViewById(R.id.refreshLayout) + headerRoot = findViewById(R.id.headerRoot) + } + + private fun prepareToolbar() { + toolbar.navigationIcon = requireContext().drawable(R.drawable.ic_close) + .tint(requireContext().color(R.color.accent)) + + + toolbar.setTitle(R.string.account_dialog_title) + toolbar.setTitleMode(Toolbar.TitleMode.SIMPLE) + toolbar.setNavigationClickListener { dismiss() } + + MemoryCache.getUserById(UserConfig.userId)?.let { + AppGlobal.handler.post { ViewUtils.prepareNavigationHeader(headerRoot, it) } + } + } + + private fun prepareRecyclerView() { + refreshLayout.isEnabled = false + + recyclerView.layoutManager = + LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + recyclerView.setHasFixedSize(true) + + createItemsAndAdapter() + } + + private fun createItemsAndAdapter() { + val items = arrayListOf() + + SimpleMenuItem( + requireContext().drawable(R.drawable.ic_settings_outline) + .tint(requireContext().color(R.color.accent)), + requireContext().getString(R.string.navigation_settings) + ) { openSettingsScreen() }.let { items.add(it) } + + adapter = SimpleItemAdapter(requireContext(), items).also { + it.itemClickListener = this + } + + recyclerView.adapter = adapter + } + + private fun openSettingsScreen() { + startActivity(Intent(requireContext(), SettingsActivity::class.java)) + } + + override fun onItemClick(position: Int) { + val item = adapter.getItem(position) + + item.clickListener?.let { + it.onClick(requireView().findViewById(android.R.id.content)) + dismiss() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/dialog/ProfileDialog.kt b/app/src/main/java/ru/melod1n/project/vkm/dialog/ProfileDialog.kt new file mode 100644 index 00000000..3e732824 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/dialog/ProfileDialog.kt @@ -0,0 +1,115 @@ +package ru.melod1n.project.vkm.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.adapter.SimpleItemAdapter +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.item.SimpleMenuItem + +open class ProfileDialog( + private val conversation: VKConversation, + private val chatTitle: String +) : BottomSheetDialogFragment() { + + companion object { + const val TAG = "profile_bottom_sheet_dialog" + } + + private lateinit var title: TextView + private lateinit var subtitle: TextView + private lateinit var recyclerView: RecyclerView + private lateinit var root: LinearLayout + + private var adapter: SimpleItemAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setStyle(STYLE_NO_TITLE, R.style.AppTheme_ProfileDialog) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_profile_bottom, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + title = view.findViewById(R.id.profileTitle) + subtitle = view.findViewById(R.id.profileSubtitle) + recyclerView = view.findViewById(R.id.profileItemMenu) + root = view.findViewById(R.id.profileRoot) + + val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + + recyclerView.layoutManager = layoutManager + + title.text = chatTitle + + subtitle.text = getSubtitle() + + val items = ArrayList() + + items.add( + SimpleMenuItem( + ContextCompat.getDrawable( + requireContext(), + R.drawable.ic_search + )!!, "Search" + ) + ) + + createAdapter(items) + } + + private fun createAdapter(items: ArrayList) { + 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 -> "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/event/EventInfo.kt b/app/src/main/java/ru/melod1n/project/vkm/event/EventInfo.kt new file mode 100644 index 00000000..349c485e --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/event/EventInfo.kt @@ -0,0 +1,5 @@ +package ru.melod1n.project.vkm.event + +import ru.melod1n.project.vkm.api.VKApiKeys + +class EventInfo constructor(var key: VKApiKeys, var data: T? = null) \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/ArrayExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/ArrayExtensions.kt new file mode 100644 index 00000000..862c3516 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/ArrayExtensions.kt @@ -0,0 +1,13 @@ +package ru.melod1n.project.vkm.extensions + +import android.content.Context + +object ArrayExtensions { + + fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty() + + fun List.asArrayList(): ArrayList { + return ArrayList(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/ContextExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/ContextExtensions.kt new file mode 100644 index 00000000..8e57c849 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/ContextExtensions.kt @@ -0,0 +1,37 @@ +package ru.melod1n.project.vkm.extensions + +import android.content.Context +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.* +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat + +object ContextExtensions { + + fun Context.drawable(@DrawableRes resId: Int): Drawable? { + return ContextCompat.getDrawable(this, resId) + } + + @ColorInt + fun Context.color(@ColorRes resId: Int): Int { + return ContextCompat.getColor(this, resId) + } + + fun Context.font(@FontRes resId: Int): Typeface? { + return ResourcesCompat.getFont(this, resId) + } + + fun Context.string(@StringRes resId: Int): String { + return getString(resId) + } + + fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View { + return LayoutInflater.from(this).inflate(resId, root, attachToRoot) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/DrawableExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/DrawableExtensions.kt new file mode 100644 index 00000000..20719cf6 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/DrawableExtensions.kt @@ -0,0 +1,16 @@ +package ru.melod1n.project.vkm.extensions + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat + +object DrawableExtensions { + + fun Drawable?.tint(@ColorInt color: Int): Drawable? { + this?.setTint(color) + return this + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/FloatExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/FloatExtensions.kt new file mode 100644 index 00000000..9d7f58c2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/FloatExtensions.kt @@ -0,0 +1,11 @@ +package ru.melod1n.project.vkm.extensions + +import kotlin.math.roundToInt + +object FloatExtensions { + + fun Float.int(): Int { + return roundToInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/FragmentExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/FragmentExtensions.kt new file mode 100644 index 00000000..b2ce6ea8 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/FragmentExtensions.kt @@ -0,0 +1,16 @@ +package ru.melod1n.project.vkm.extensions + +import android.view.View +import androidx.fragment.app.Fragment + +object FragmentExtensions { + + fun Fragment.findViewById(resId: Int): T { + return requireView().findViewById(resId) + } + + fun Fragment.runOnUiThread(runnable: Runnable) { + requireActivity().runOnUiThread(runnable) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/ImageViewExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/ImageViewExtensions.kt new file mode 100644 index 00000000..342e550f --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/ImageViewExtensions.kt @@ -0,0 +1,55 @@ +package ru.melod1n.project.vkm.extensions + +import android.graphics.drawable.Drawable +import android.util.Log +import android.widget.ImageView +import com.squareup.picasso.Callback +import com.squareup.picasso.Picasso +import ru.melod1n.project.vkm.BuildConfig + +object ImageViewExtensions { + + fun ImageView.loadImage( + sourceUrl: String, + placeholder: Drawable? = null, + callback: Callback? = null + ) { + if (sourceUrl.trim().isEmpty()) { + if (BuildConfig.DEBUG) { + Log.d("ImageView", "sourceUrl is empty") + } + return + } + + val builder = Picasso.get().load(sourceUrl) + + placeholder?.let { builder.placeholder(it) } + + try { + builder.into(this, object : Callback { + override fun onSuccess() { + if (BuildConfig.DEBUG) { + Log.d("ImageView", "loaded photo from $sourceUrl") + } + + callback?.onSuccess() + } + + override fun onError(e: Exception?) { + if (BuildConfig.DEBUG) { + Log.d("ImageView", "error loading photo from $sourceUrl") + } + + callback?.onError(e) + } + }) + } catch (e: Exception) { + e.printStackTrace() + + if (BuildConfig.DEBUG) { + Log.d("ImageView", "Error loading photo from $sourceUrl") + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/StringExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/StringExtensions.kt new file mode 100644 index 00000000..71d6f2fb --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/StringExtensions.kt @@ -0,0 +1,11 @@ +package ru.melod1n.project.vkm.extensions + +import java.util.* + +object StringExtensions { + + fun String.lowerCase(): String { + return toLowerCase(Locale.getDefault()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/extensions/TextViewExtensions.kt b/app/src/main/java/ru/melod1n/project/vkm/extensions/TextViewExtensions.kt new file mode 100644 index 00000000..ffaac8e3 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/extensions/TextViewExtensions.kt @@ -0,0 +1,17 @@ +package ru.melod1n.project.vkm.extensions + +import android.widget.TextView +import com.google.android.material.textfield.TextInputLayout + +object TextViewExtensions { + + fun TextView.clear() { + text = "" + } + + fun TextInputLayout.clear() { + editText?.setText("") + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentConversations.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentConversations.kt new file mode 100644 index 00000000..3e039567 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentConversations.kt @@ -0,0 +1,186 @@ +package ru.melod1n.project.vkm.fragment + +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.MessagesActivity +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.base.BaseFragment +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById +import ru.melod1n.project.vkm.fragment.ui.presenter.ConversationsPresenter +import ru.melod1n.project.vkm.fragment.ui.view.ConversationsView +import ru.melod1n.project.vkm.util.ViewUtils +import ru.melod1n.project.vkm.widget.Toolbar + + +@Suppress("UNCHECKED_CAST") +class FragmentConversations : BaseFragment(), ConversationsView { + + private lateinit var presenter: ConversationsPresenter + + private lateinit var toolbar: Toolbar + private lateinit var refreshLayout: SwipeRefreshLayout + private lateinit var recyclerView: RecyclerView + private lateinit var progressBar: ProgressBar + + private lateinit var noItemsView: LinearLayout + private lateinit var noInternetView: LinearLayout + private lateinit var errorView: LinearLayout + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_conversations, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initViews() + + prepareToolbar() + prepareRecyclerView() + prepareRefreshLayout() + + presenter = ConversationsPresenter(this) + presenter.setup(recyclerView, refreshLayout) + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + recyclerView = findViewById(R.id.recyclerView) + refreshLayout = findViewById(R.id.refreshLayout) + progressBar = findViewById(R.id.progressBar) + + noItemsView = findViewById(R.id.noItemsView) + noInternetView = findViewById(R.id.noInternetView) + errorView = findViewById(R.id.errorView) + } + + private fun prepareToolbar() { + initToolbar(R.id.toolbar) + toolbar.title = getString(R.string.navigation_conversations) + setProfileAvatar() + + TaskManager.addOnEventListener(object : TaskManager.OnEventListener { + override fun onNewEvent(info: EventInfo<*>) { + if (info.key == VKApiKeys.UPDATE_USER) { + val userIds = info.data as ArrayList + + if (userIds.contains(UserConfig.userId)) { + setProfileAvatar() + } + } + } + }) + } + + private fun prepareRefreshLayout() { + refreshLayout.setColorSchemeResources(R.color.accent) + } + + private fun prepareRecyclerView() { + val manager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + + val decoration = DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) + + decoration.setDrawable( + ColorDrawable( + ContextCompat.getColor( + requireContext(), + R.color.divider + ) + ) + ) + + recyclerView.setHasFixedSize(true) + recyclerView.itemAnimator = null + recyclerView.addItemDecoration(decoration) + + recyclerView.layoutManager = manager + } + + private fun setProfileAvatar() { + TaskManager.execute { + AppGlobal.database.users.getById(UserConfig.userId)?.let { + if (it.photo100.isNotEmpty()) { + runOnUi { + toolbar.getAvatar().setImageURI(it.photo100) + } + } + } + } + } + + override fun openChat(extras: Bundle) { + startActivity(Intent(requireContext(), MessagesActivity::class.java).putExtras(extras)) + } + + override fun showErrorSnackbar(t: Throwable) { + ViewUtils.showErrorSnackbar(requireView(), t) + } + + override fun prepareNoItemsView() { + } + + override fun showNoItemsView() { + noItemsView.isVisible = true + } + + override fun hideNoItemsView() { + noItemsView.isVisible = false + } + + override fun prepareNoInternetView() { + } + + override fun showNoInternetView() { + noInternetView.isVisible = true + } + + override fun hideNoInternetView() { + noInternetView.isVisible = false + } + + override fun prepareErrorView() { + + } + + override fun showErrorView() { + errorView.isVisible = true + } + + override fun hideErrorView() { + errorView.isVisible = false + } + + override fun showProgressBar() { + progressBar.isVisible = true + } + + override fun hideProgressBar() { + progressBar.isVisible = false + } + + override fun showRefreshLayout() { + refreshLayout.isRefreshing = true + } + + override fun hideRefreshLayout() { + refreshLayout.isRefreshing = false + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentFriends.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentFriends.kt new file mode 100644 index 00000000..5f7a99c2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentFriends.kt @@ -0,0 +1,191 @@ +package ru.melod1n.project.vkm.fragment + +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.MessagesActivity +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApiKeys +import ru.melod1n.project.vkm.base.BaseFragment +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.event.EventInfo +import ru.melod1n.project.vkm.extensions.FragmentExtensions.findViewById +import ru.melod1n.project.vkm.fragment.ui.presenter.FriendsPresenter +import ru.melod1n.project.vkm.fragment.ui.view.FriendsView +import ru.melod1n.project.vkm.util.ViewUtils +import ru.melod1n.project.vkm.widget.Toolbar + +class FragmentFriends(private val userId: Int = 0) : BaseFragment(), FriendsView { + + private lateinit var presenter: FriendsPresenter + + private lateinit var toolbar: Toolbar + private lateinit var recyclerView: RecyclerView + private lateinit var refreshLayout: SwipeRefreshLayout + private lateinit var progressBar: ProgressBar + + private lateinit var noItemsView: LinearLayout + private lateinit var noInternetView: LinearLayout + private lateinit var errorView: LinearLayout + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_friends, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initViews() + + prepareToolbar() + prepareRecyclerView() + prepareRefreshLayout() + + presenter = FriendsPresenter(this) + presenter.setup(userId, recyclerView, refreshLayout) + } + + private fun initViews() { + toolbar = findViewById(R.id.toolbar) + recyclerView = findViewById(R.id.recyclerView) + refreshLayout = findViewById(R.id.refreshLayout) + progressBar = findViewById(R.id.progressBar) + + noItemsView = findViewById(R.id.noItemsView) + noInternetView = findViewById(R.id.noInternetView) + errorView = findViewById(R.id.errorView) + } + + private fun prepareToolbar() { + initToolbar(R.id.toolbar) + toolbar.title = getString(R.string.navigation_friends) + setProfileAvatar() + + toolbar.inflateMenu(R.menu.fragment_friends) + + TaskManager.addOnEventListener(object : TaskManager.OnEventListener { + override fun onNewEvent(info: EventInfo<*>) { + if (info.key == VKApiKeys.UPDATE_USER) { + val userId = info.data as ArrayList + + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentImportant.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentImportant.kt new file mode 100644 index 00000000..1cdfb056 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentImportant.kt @@ -0,0 +1,21 @@ +package ru.melod1n.project.vkm.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseFragment + + +class FragmentImportant : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_important, container, false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSearch.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSearch.kt new file mode 100644 index 00000000..3d1b9d3e --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSearch.kt @@ -0,0 +1,8 @@ +package ru.melod1n.project.vkm.fragment + +import ru.melod1n.project.vkm.base.BaseFragment + +class FragmentSearch : BaseFragment() { + inner class SearchConversations : BaseFragment() + inner class SearchMessages : BaseFragment() +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSettings.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSettings.kt new file mode 100644 index 00000000..81bbd6e0 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/FragmentSettings.kt @@ -0,0 +1,218 @@ +package ru.melod1n.project.vkm.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 ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.DropUserDataActivity +import ru.melod1n.project.vkm.activity.UpdateActivity +import ru.melod1n.project.vkm.base.BaseActivity +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.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(CATEGORY_GENERAL) + general?.onPreferenceClickListener = rootLayoutClickListener + + val account = findPreference(CATEGORY_ACCOUNT) + account?.onPreferenceClickListener = rootLayoutClickListener + + val logout = findPreference(KEY_ACCOUNT_LOGOUT) + logout?.onPreferenceClickListener = this + + val about = findPreference(CATEGORY_ABOUT) + about?.onPreferenceClickListener = rootLayoutClickListener + + val appVersion = findPreference(KEY_APP_VERSION) + appVersion?.onPreferenceClickListener = this + + val appearance = findPreference(CATEGORY_APPEARANCE) + appearance?.onPreferenceClickListener = rootLayoutClickListener + + val extendedConversations = findPreference(KEY_EXTENDED_CONVERSATIONS) + extendedConversations?.onPreferenceChangeListener = this + + val theme = findPreference(KEY_THEME) + theme?.onPreferenceChangeListener = this + + val debug = findPreference(CATEGORY_DEBUG) + debug?.onPreferenceClickListener = rootLayoutClickListener + updateDebugCategoryVisibility() + + val clearUsersGroupsCache = findPreference(KEY_CLEAR_USERS_GROUPS_CACHE) + clearUsersGroupsCache?.onPreferenceClickListener = this + + applyTintInPreferenceScreen(preferenceScreen) + } + + override fun onResume() { + super.onResume() + + updateDebugCategoryVisibility() + } + + private fun updateDebugCategoryVisibility() { + findPreference(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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/LoginFragment.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/LoginFragment.kt new file mode 100644 index 00000000..c3a09024 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/LoginFragment.kt @@ -0,0 +1,157 @@ +package ru.melod1n.project.vkm.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 ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseFragment +import ru.melod1n.project.vkm.fragment.ui.presenter.LoginPresenter +import ru.melod1n.project.vkm.fragment.ui.view.LoginView +import ru.melod1n.project.vkm.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?) { + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ValidationFragment.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ValidationFragment.kt new file mode 100644 index 00000000..d521b088 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ValidationFragment.kt @@ -0,0 +1,88 @@ +package ru.melod1n.project.vkm.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 ru.melod1n.project.vkm.api.VKAuth +import ru.melod1n.project.vkm.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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/ConversationsPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/ConversationsPresenter.kt new file mode 100644 index 00000000..bca6e1de --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/ConversationsPresenter.kt @@ -0,0 +1,254 @@ +package ru.melod1n.project.vkm.fragment.ui.presenter + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.activity.MessagesActivity +import ru.melod1n.project.vkm.adapter.ConversationsAdapter +import ru.melod1n.project.vkm.adapter.diffutil.ConversationsCallback +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpPresenter +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.common.TimeManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.fragment.ui.repository.ConversationsRepository +import ru.melod1n.project.vkm.fragment.ui.view.ConversationsView +import ru.melod1n.project.vkm.listener.ItemClickListener +import ru.melod1n.project.vkm.listener.ItemLongClickListener +import ru.melod1n.project.vkm.util.AndroidUtils +import ru.melod1n.project.vkm.util.ArrayUtils +import java.util.* + +class ConversationsPresenter(viewState: ConversationsView) : + MvpPresenter( + 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) + + getCachedConversations(0, DEFAULT_CONVERSATIONS_COUNT, object : MvpOnLoadListener { + 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 { + 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 { + 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? = null + ) { + setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING) + + repository.getCachedConversations(offset, count, + object : MvpOnLoadListener> { + override fun onResponse(response: ArrayList) { + 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? = 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> { + override fun onResponse(response: ArrayList) { + 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, 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) } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/FriendsPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/FriendsPresenter.kt new file mode 100644 index 00000000..4e57d7c6 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/FriendsPresenter.kt @@ -0,0 +1,226 @@ +package ru.melod1n.project.vkm.fragment.ui.presenter + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import ru.melod1n.project.vkm.activity.MessagesActivity +import ru.melod1n.project.vkm.adapter.UsersAdapter +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpPresenter +import ru.melod1n.project.vkm.fragment.ui.repository.FriendsRepository +import ru.melod1n.project.vkm.fragment.ui.view.FriendsView +import ru.melod1n.project.vkm.listener.ItemClickListener +import ru.melod1n.project.vkm.util.AndroidUtils +import ru.melod1n.project.vkm.util.ArrayUtils + +class FriendsPresenter(viewState: FriendsView) : + MvpPresenter( + 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 { + override fun onResponse(response: Any?) { + setState(if (adapter.isEmpty()) 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? = null + ) { + setState(if (adapter.isEmpty()) ListState.EMPTY_LOADING else ListState.FILLED_LOADING) + + repository.getCachedFriends(userId, offset, count, onlyOnline, + object : MvpOnLoadListener> { + override fun onResponse(response: ArrayList) { + 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? = 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> { + override fun onResponse(response: ArrayList) { + 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 { + 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 { + 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, 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) { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/LoginPresenter.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/LoginPresenter.kt new file mode 100644 index 00000000..9ab7a214 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/presenter/LoginPresenter.kt @@ -0,0 +1,184 @@ +package ru.melod1n.project.vkm.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.squareup.picasso.Picasso +import org.json.JSONObject +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.activity.MainActivity +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpPresenter +import ru.melod1n.project.vkm.extensions.FragmentExtensions.runOnUiThread +import ru.melod1n.project.vkm.fragment.FragmentConversations +import ru.melod1n.project.vkm.fragment.LoginFragment +import ru.melod1n.project.vkm.fragment.ValidationFragment +import ru.melod1n.project.vkm.fragment.ui.repository.LoginRepository +import ru.melod1n.project.vkm.fragment.ui.view.LoginView + + +class LoginPresenter( + viewState: LoginView +) : MvpPresenter( + 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? = null + ) { + lastEmail = email + lastPassword = password + + repository.login(requireContext(), email, password, captcha, + object : MvpOnLoadListener { + 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? = 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.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() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/ConversationsRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/ConversationsRepository.kt new file mode 100644 index 00000000..48b52aac --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/ConversationsRepository.kt @@ -0,0 +1,102 @@ +package ru.melod1n.project.vkm.fragment.ui.repository + +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.api.model.VKConversation +import ru.melod1n.project.vkm.api.model.VKMessage +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpRepository +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList +import ru.melod1n.project.vkm.listener.OnResponseListener + +class ConversationsRepository : MvpRepository() { + + fun loadConversations( + offset: Int, count: Int, + listener: MvpOnLoadListener> + ) { + TaskManager.execute { + VKApi.messages() + .getConversations() + .filter("all") + .extended(true) + .fields(VKUser.DEFAULT_FIELDS) + .offset(offset) + .count(count) + .executeArray(VKConversation::class.java, + object : OnResponseListener> { + override fun onResponse(response: ArrayList) { + 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> + ) { + TaskManager.execute { + val conversations = MemoryCache.getConversations().asArrayList() + + VKUtil.sortConversationsByDate(conversations, true) + + sendResponse(listener, conversations) + } + } + + private fun fillConversationsWithProfilesAndGroups(conversations: ArrayList) { + 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) { + val messages = arrayListOf() + + for (conversation in conversations) { + messages.add(conversation.lastMessage) + } + + MemoryCache.putMessages(messages) + MemoryCache.putConversations(conversations) + } +} diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/FriendsRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/FriendsRepository.kt new file mode 100644 index 00000000..d41d10b1 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/FriendsRepository.kt @@ -0,0 +1,89 @@ +package ru.melod1n.project.vkm.fragment.ui.repository + +import android.util.Log +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.api.model.VKFriend +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpRepository +import ru.melod1n.project.vkm.common.TaskManager +import ru.melod1n.project.vkm.database.MemoryCache +import ru.melod1n.project.vkm.listener.OnResponseListener + +class FriendsRepository : MvpRepository() { + + fun loadFriends( + userId: Int, + offset: Int, + count: Int, + listener: MvpOnLoadListener> + ) { + TaskManager.execute { + VKApi.friends() + .get() + .order("hints") + .userId(userId) + .fields(VKUser.DEFAULT_FIELDS) + .count(count) + .offset(offset) + .executeArray(VKUser::class.java, + object : OnResponseListener> { + override fun onResponse(response: ArrayList) { + 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> + ) { + 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() + + 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) { + MemoryCache.putUsers(users) + + val friends = ArrayList() + + for (user in users) { + friends.add(VKFriend(user.userId, userId)) + } + + MemoryCache.putFriends(friends) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/LoginRepository.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/LoginRepository.kt new file mode 100644 index 00000000..e9ae5137 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/repository/LoginRepository.kt @@ -0,0 +1,74 @@ +package ru.melod1n.project.vkm.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 org.json.JSONObject +import org.jsoup.Jsoup +import ru.melod1n.project.vkm.api.VKAuth +import ru.melod1n.project.vkm.base.mvp.MvpOnLoadListener +import ru.melod1n.project.vkm.base.mvp.MvpRepository + +class LoginRepository : MvpRepository() { + + fun login( + context: Context, + email: String, + password: String, + captcha: String, + onLoadListener: MvpOnLoadListener + ) { + 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" + + "(''+document.getElementsByTagName('html')[0].innerHTML+'');" + ) + } + } + + 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) { + @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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/ConversationsView.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/ConversationsView.kt new file mode 100644 index 00000000..38bdbfb1 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/ConversationsView.kt @@ -0,0 +1,10 @@ +package ru.melod1n.project.vkm.fragment.ui.view + +import android.os.Bundle +import ru.melod1n.project.vkm.base.mvp.MvpView + +interface ConversationsView : MvpView { + + fun openChat(extras: Bundle) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/FriendsView.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/FriendsView.kt new file mode 100644 index 00000000..44bc2cf8 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/FriendsView.kt @@ -0,0 +1,10 @@ +package ru.melod1n.project.vkm.fragment.ui.view + +import android.os.Bundle +import ru.melod1n.project.vkm.base.mvp.MvpView + +interface FriendsView : MvpView { + + fun openChat(extras: Bundle) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/LoginView.kt b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/LoginView.kt new file mode 100644 index 00000000..336c98f7 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/fragment/ui/view/LoginView.kt @@ -0,0 +1,11 @@ +package ru.melod1n.project.vkm.fragment.ui.view + +import ru.melod1n.project.vkm.base.mvp.MvpView + +interface LoginView : MvpView { + + fun initViews() + + fun prepareViews() + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/io/BytesOutputStream.kt b/app/src/main/java/ru/melod1n/project/vkm/io/BytesOutputStream.kt new file mode 100644 index 00000000..d31d58fb --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/io/BytesOutputStream.kt @@ -0,0 +1,10 @@ +package ru.melod1n.project.vkm.io + +import java.io.ByteArrayOutputStream + +class BytesOutputStream : ByteArrayOutputStream { + constructor() : super(8192) + constructor(size: Int) : super(size) + + val byteArray: ByteArray = buf +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/io/Charsets.kt b/app/src/main/java/ru/melod1n/project/vkm/io/Charsets.kt new file mode 100644 index 00000000..39cd6edf --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/io/Charsets.kt @@ -0,0 +1,12 @@ +package ru.melod1n.project.vkm.io + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +object Charsets { + + val ASCII: Charset = StandardCharsets.US_ASCII + + val UTF_8: Charset = StandardCharsets.UTF_8 + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/io/EasyStreams.kt b/app/src/main/java/ru/melod1n/project/vkm/io/EasyStreams.kt new file mode 100644 index 00000000..7f27e929 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/io/EasyStreams.kt @@ -0,0 +1,174 @@ +package ru.melod1n.project.vkm.io + +import org.jetbrains.annotations.Contract +import java.io.* +import java.nio.charset.Charset +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import kotlin.math.max + +object EasyStreams { + + const val BUFFER_SIZE = 8192 + const val CHAR_BUFFER_SIZE = 4096 + + @JvmOverloads + @Throws(IOException::class) + fun read(from: InputStream, encoding: Charset? = Charsets.UTF_8): String { + return read(InputStreamReader(from, encoding)) + } + + @JvmStatic + @Throws(IOException::class) + fun read(from: Reader): String { + val builder = StringWriter(CHAR_BUFFER_SIZE) + return try { + copy(from, builder) + builder.toString() + } finally { + close(from) + } + } + + @JvmStatic + @Throws(IOException::class) + fun readBytes(from: InputStream): ByteArray { + val output = ByteArrayOutputStream(max(from.available(), BUFFER_SIZE)) + try { + copy(from, output) + } finally { + close(from) + } + return output.toByteArray() + } + + @Throws(IOException::class) + fun write(from: ByteArray?, to: OutputStream) { + try { + to.write(from) + to.flush() + } finally { + close(to) + } + } + + @Throws(IOException::class) + fun write(from: String?, to: OutputStream?) { + write(from, OutputStreamWriter(to, Charsets.UTF_8)) + } + + @Throws(IOException::class) + fun write(from: CharArray?, to: Writer) { + try { + to.write(from) + to.flush() + } finally { + close(to) + } + } + + @JvmStatic + @Throws(IOException::class) + fun write(from: String?, to: Writer) { + try { + to.write(from) + to.flush() + } finally { + close(to) + } + } + + @Throws(IOException::class) + fun copy(from: Reader, to: Writer): Long { + val buffer = CharArray(CHAR_BUFFER_SIZE) + var read: Int + var total: Long = 0 + while (from.read(buffer).also { read = it } != -1) { + to.write(buffer, 0, read) + total += read.toLong() + } + return total + } + + @Throws(IOException::class) + fun copy(from: InputStream, to: OutputStream): Long { + val buffer = ByteArray(BUFFER_SIZE) + var read: Int + var total: Long = 0 + while (from.read(buffer).also { read = it } != -1) { + to.write(buffer, 0, read) + total += read.toLong() + } + return total + } + + fun buffer(input: InputStream?): BufferedInputStream { + return buffer(input, BUFFER_SIZE) + } + + @Contract("null, _ -> new") + fun buffer(input: InputStream?, size: Int): BufferedInputStream { + return if (input is BufferedInputStream) input else BufferedInputStream(input, size) + } + + fun buffer(output: OutputStream?): BufferedOutputStream { + return buffer(output, BUFFER_SIZE) + } + + @Contract("null, _ -> new") + fun buffer(output: OutputStream?, size: Int): BufferedOutputStream { + return if (output is BufferedOutputStream) output else BufferedOutputStream(output, size) + } + + fun buffer(input: Reader?): BufferedReader { + return buffer(input, CHAR_BUFFER_SIZE) + } + + @Contract("null, _ -> new") + fun buffer(input: Reader?, size: Int): BufferedReader { + return if (input is BufferedReader) input else BufferedReader(input, size) + } + + fun buffer(output: Writer?): BufferedWriter { + return buffer(output, CHAR_BUFFER_SIZE) + } + + @Contract("null, _ -> new") + fun buffer(output: Writer?, size: Int): BufferedWriter { + return if (output is BufferedWriter) output else BufferedWriter(output, size) + } + + @Throws(IOException::class) + fun gzip(input: InputStream?): GZIPInputStream { + return gzip(input, BUFFER_SIZE) + } + + @Contract("null, _ -> new") + @Throws(IOException::class) + fun gzip(input: InputStream?, size: Int): GZIPInputStream { + return if (input is GZIPInputStream) input else GZIPInputStream(input, size) + } + + @Throws(IOException::class) + fun gzip(input: OutputStream?): GZIPOutputStream { + return gzip(input, BUFFER_SIZE) + } + + @Contract("null, _ -> new") + @Throws(IOException::class) + fun gzip(input: OutputStream?, size: Int): GZIPOutputStream { + return if (input is GZIPOutputStream) input else GZIPOutputStream(input, size) + } + + fun close(c: Closeable?): Boolean { + if (c != null) { + try { + c.close() + return true + } catch (e: IOException) { + e.printStackTrace() + } + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/io/FileStreams.kt b/app/src/main/java/ru/melod1n/project/vkm/io/FileStreams.kt new file mode 100644 index 00000000..8b6383e2 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/io/FileStreams.kt @@ -0,0 +1,98 @@ +package ru.melod1n.project.vkm.io + +import org.jetbrains.annotations.Contract +import ru.melod1n.project.vkm.io.EasyStreams.read +import ru.melod1n.project.vkm.io.EasyStreams.write +import java.io.* +import java.math.BigInteger + +object FileStreams { + + val lineSeparatorChar = lineSeparator()[0] + + const val ONE_KB = 1024 + const val ONE_MB = ONE_KB * 1024 + const val ONE_GB = ONE_MB * 1024 + const val ONE_TB = ONE_GB * 1024L + const val ONE_PB = ONE_TB * 1024L + const val ONE_EB = ONE_PB * 1024L + + val ONE_ZB: BigInteger = BigInteger.valueOf(ONE_EB).multiply(BigInteger.valueOf(1024L)) + val ONE_YB: BigInteger = ONE_ZB.multiply(BigInteger.valueOf(1024L)) + + @Throws(IOException::class) + fun read(from: File?): String { + return read(reader(from)) + } + + @Throws(IOException::class) + fun write(from: String?, to: File?) { + write(from, writer(to)) + } + + @Throws(IOException::class) + fun write(from: ByteArray?, to: File?) { + write(from, FileOutputStream(to)) + } + + @Throws(IOException::class) + fun append(from: ByteArray?, to: File?) { + write(from, FileOutputStream(to, true)) + } + + @Throws(IOException::class) + fun append(from: CharArray?, to: File?) { + write(from, FileWriter(to, true)) + } + + @Throws(IOException::class) + fun append(from: CharSequence, to: File?) { + write(if (from is String) from else from.toString(), FileWriter(to, true)) + } + + fun delete(dir: File) { + if (dir.isDirectory) { + val files = dir.listFiles() ?: return + for (file in files) { + delete(file) + } + } else { + dir.delete() + } + } + + fun lineSeparator(): String { + return System.lineSeparator() + } + + fun search(dir: File, name: String?): File? { + require(dir.isDirectory) { "dir can't be file." } + + val files = dir.listFiles() ?: return null + + if (files.isEmpty()) { + return null + } + + for (file in files) { + if (file.isDirectory) { + search(file, name) + } else if (file.name.contains(name!!)) { + return file + } + } + return null + } + + @Contract("_ -> new") + @Throws(FileNotFoundException::class) + fun reader(from: File?): Reader { + return InputStreamReader(FileInputStream(from), Charsets.UTF_8) + } + + @Contract("_ -> new") + @Throws(FileNotFoundException::class) + fun writer(to: File?): Writer { + return OutputStreamWriter(FileOutputStream(to), Charsets.UTF_8) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/item/SimpleMenuItem.kt b/app/src/main/java/ru/melod1n/project/vkm/item/SimpleMenuItem.kt new file mode 100644 index 00000000..4c50d755 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/item/SimpleMenuItem.kt @@ -0,0 +1,6 @@ +package ru.melod1n.project.vkm.item + +import android.graphics.drawable.Drawable +import android.view.View + +data class SimpleMenuItem(val icon: Drawable?, val title: String, var clickListener: View.OnClickListener? = null) diff --git a/app/src/main/java/ru/melod1n/project/vkm/listener/ItemClickListener.kt b/app/src/main/java/ru/melod1n/project/vkm/listener/ItemClickListener.kt new file mode 100644 index 00000000..bef67d5e --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/listener/ItemClickListener.kt @@ -0,0 +1,5 @@ +package ru.melod1n.project.vkm.listener + +interface ItemClickListener { + fun onItemClick(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/listener/ItemLongClickListener.kt b/app/src/main/java/ru/melod1n/project/vkm/listener/ItemLongClickListener.kt new file mode 100644 index 00000000..1f1d04a0 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/listener/ItemLongClickListener.kt @@ -0,0 +1,5 @@ +package ru.melod1n.project.vkm.listener + +interface ItemLongClickListener { + fun onItemLongClick(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/listener/OnResponseListener.kt b/app/src/main/java/ru/melod1n/project/vkm/listener/OnResponseListener.kt new file mode 100644 index 00000000..d833d44e --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/listener/OnResponseListener.kt @@ -0,0 +1,9 @@ +package ru.melod1n.project.vkm.listener + +interface OnResponseListener { + + fun onResponse(response: T) + + fun onError(t: Throwable) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/model/NewUpdateInfo.kt b/app/src/main/java/ru/melod1n/project/vkm/model/NewUpdateInfo.kt new file mode 100644 index 00000000..a11057a4 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/model/NewUpdateInfo.kt @@ -0,0 +1,30 @@ +package ru.melod1n.project.vkm.model + +import org.json.JSONObject + +class NewUpdateInfo() { + + var id: Int = 0 + var version: String = "" + var code: Int = 0 + var time: Int = 0 + var changelog: String = "" + +// var branchId: Int = 0 +// var branchName: String = "" + + var downloadLink: String = "" + +// var state: Boolean = true + + constructor(o: JSONObject) : this() { + id = o.optInt("id") + version = o.optString("version") + code = o.optInt("code") + time = o.optInt("time") + changelog = o.optString("changelog") + + downloadLink = o.optString("download") + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/model/UpdateInfo.kt b/app/src/main/java/ru/melod1n/project/vkm/model/UpdateInfo.kt new file mode 100644 index 00000000..2555fe7c --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/model/UpdateInfo.kt @@ -0,0 +1,21 @@ +package ru.melod1n.project.vkm.model + +import org.json.JSONObject + +class UpdateInfo() { + + var version: String = "" + var code: Int = 0 + var changelog: String = "" + var downloadLink: String = "" + var date: Int = 0 + + constructor(o: JSONObject): this() { + version = o.optString("lastVersionName") + code = o.optInt("lastVersionCode") + changelog = o.optString("changelog") + downloadLink = o.optString("downloadLink") + date = o.optInt("buildDate") + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/net/HttpRequest.kt b/app/src/main/java/ru/melod1n/project/vkm/net/HttpRequest.kt new file mode 100644 index 00000000..da24e89d --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/net/HttpRequest.kt @@ -0,0 +1,118 @@ +package ru.melod1n.project.vkm.net + +import androidx.collection.ArrayMap +import androidx.collection.arrayMapOf +import ru.melod1n.project.vkm.io.EasyStreams.gzip +import ru.melod1n.project.vkm.io.EasyStreams.read +import ru.melod1n.project.vkm.io.EasyStreams.readBytes +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLEncoder + +class HttpRequest( + private val url: String, + private val method: String, + private val params: ArrayMap = arrayMapOf() +) { + companion object { + const val GET = "GET" + const val POST = "POST" + + operator fun get( + url: String, + params: ArrayMap = arrayMapOf() + ): HttpRequest { + return HttpRequest(url, GET, params) + } + + operator fun get(url: String): HttpRequest { + return Companion[url, ArrayMap()] + } + + } + + private var connection: HttpURLConnection? = null + + @Throws(IOException::class) + fun asString(): String { + val input = getStream() + + val content = read(input) + + connection?.disconnect() + + return content + } + + @Throws(IOException::class) + fun asBytes(): ByteArray { + val input = getStream() + val content = readBytes(input) + + connection?.disconnect() + + return content + } + + @Throws(IOException::class) + fun getStream(): InputStream { + if (connection == null) { + connection = createConnection() + } + + var input = connection!!.inputStream + val encoding = connection!!.getHeaderField("Content-Encoding") + if ("gzip".equals(encoding, ignoreCase = true)) { + input = gzip(input) + } + + return input + } + + @Throws(UnsupportedEncodingException::class) + private fun getParams(): String { + val buffer = StringBuilder() + + for (i in 0 until params.size) { + val key = params.keyAt(i) + val value = params.valueAt(i) + buffer.append(key).append("=") + buffer.append(URLEncoder.encode(value, "UTF-8")) + buffer.append("&") + } + return buffer.toString() + } + + @Throws(UnsupportedEncodingException::class) + private fun getUrl(): String { + return if (params.isNotEmpty() && "GET".equals(method, ignoreCase = true)) { + url + "?" + getParams() + } else url + } + + @Throws(IOException::class) + private fun createConnection(): HttpURLConnection? { + connection = URL(getUrl()).openConnection() as HttpURLConnection + connection!!.readTimeout = 60000 + connection!!.connectTimeout = 60000 + connection!!.useCaches = true + connection!!.doInput = true + connection!!.doOutput = + !GET.equals(method, ignoreCase = true) + connection!!.requestMethod = method + connection!!.setRequestProperty("Accept-Encoding", "gzip") + return connection + } + + override fun toString(): String { + try { + return asString() + } catch (e: IOException) { + e.printStackTrace() + } + return "" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/receiver/DownloadUpdateReceiver.kt b/app/src/main/java/ru/melod1n/project/vkm/receiver/DownloadUpdateReceiver.kt new file mode 100644 index 00000000..d404c3ba --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/receiver/DownloadUpdateReceiver.kt @@ -0,0 +1,17 @@ +package ru.melod1n.project.vkm.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import ru.melod1n.project.vkm.listener.OnResponseListener + +open class DownloadUpdateReceiver : BroadcastReceiver() { + + var listener: OnResponseListener? = null + + override fun onReceive(context: Context?, intent: Intent?) { + listener?.onResponse(null) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/receiver/MinuteReceiver.kt b/app/src/main/java/ru/melod1n/project/vkm/receiver/MinuteReceiver.kt new file mode 100644 index 00000000..28865f77 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/receiver/MinuteReceiver.kt @@ -0,0 +1,14 @@ +package ru.melod1n.project.vkm.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import ru.melod1n.project.vkm.common.TimeManager + +class MinuteReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context?, intent: Intent?) { + TimeManager.broadcastMinute() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/service/LongPollService.kt b/app/src/main/java/ru/melod1n/project/vkm/service/LongPollService.kt new file mode 100644 index 00000000..7e184742 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/service/LongPollService.kt @@ -0,0 +1,136 @@ +package ru.melod1n.project.vkm.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.collection.arrayMapOf +import org.json.JSONArray +import org.json.JSONObject +import ru.melod1n.project.vkm.api.UserConfig +import ru.melod1n.project.vkm.api.VKApi +import ru.melod1n.project.vkm.api.VKLongPollParser +import ru.melod1n.project.vkm.api.model.VKLongPollServer +import ru.melod1n.project.vkm.concurrent.LowThread +import ru.melod1n.project.vkm.net.HttpRequest +import ru.melod1n.project.vkm.util.AndroidUtils + +@Deprecated("Absolutely obsolete") +class LongPollService : Service() { + private var thread: Thread? = null + private var running = false + + override fun onCreate() { + super.onCreate() + + running = false + + thread = LowThread(Updater()) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (flags and START_FLAG_RETRY == 0) { + Log.w(TAG, "Retry launch!") + } else { + Log.d(TAG, "Simple launch") + } + if (running) return START_STICKY + running = true + + try { + thread?.start() + } catch (e: Exception) { + e.printStackTrace() + } + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + + running = false + + thread?.interrupt() + } + + private inner class Updater : Runnable { + override fun run() { + + var server: VKLongPollServer? = null + + while (running && UserConfig.isLoggedIn()) { + if (!AndroidUtils.hasConnection()) { + try { + Thread.sleep(5000) + } catch (e: InterruptedException) { + e.printStackTrace() + } + continue + } + try { + if (server == null) { + server = VKApi.messages().getLongPollServer() + .execute(VKLongPollServer::class.java)!![0] + } + + val response = getResponse(server) + if (response.has("failed")) { + Log.w(TAG, "Failed get response") + Thread.sleep(1000) + server = null + continue + } + + val tsResponse = response.optLong("ts") + val updates = response.getJSONArray("updates") + + Log.i(TAG, "updates: $updates") + + server.ts = tsResponse + + if (updates.length() != 0) { + process(updates) + } + } catch (e: Exception) { + e.printStackTrace() + try { + Thread.sleep(5000) + server = null + } catch (e1: InterruptedException) { + e1.printStackTrace() + } + } + } + } + + @Throws(Exception::class) + private fun getResponse(server: VKLongPollServer): JSONObject { + val params = arrayMapOf() + params["act"] = "a_check" + params["key"] = server.key + params["ts"] = server.ts.toString() + params["wait"] = "10" + params["mode"] = "490" + params["version"] = "9" + + val buffer = HttpRequest["https://" + server.server, params].asString() + + return JSONObject(buffer) + } + + @WorkerThread + private fun process(updates: JSONArray) { + VKLongPollParser.parse(updates) + } + } + + companion object { + private const val TAG = "LongPollService" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/AndroidUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/AndroidUtils.kt new file mode 100644 index 00000000..34bd5325 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/AndroidUtils.kt @@ -0,0 +1,82 @@ +package ru.melod1n.project.vkm.util + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.net.NetworkCapabilities +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.util.DisplayMetrics +import ru.melod1n.project.vkm.BuildConfig +import ru.melod1n.project.vkm.common.AppGlobal + +object AndroidUtils { + + fun px(dp: Float): Float { + return dp * (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) + } + + fun px(dp: Int) = px(dp.toFloat()) + + fun dp(px: Float): Float { + return px / (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) + } + + fun dp(px: Int) = dp(px.toFloat()) + + fun hasConnection(): Boolean { + val network = AppGlobal.connectivityManager.activeNetwork ?: return false + val activeNetwork = + AppGlobal.connectivityManager.getNetworkCapabilities(network) ?: return false + + return when { + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true + else -> false + } + } + + fun getDisplayWidth(): Int { + return AppGlobal.resources.displayMetrics.widthPixels + } + + fun getDisplayHeight(): Int { + return AppGlobal.resources.displayMetrics.heightPixels + } + + fun isDeveloperSettingsEnabled(context: Context) = Settings.Secure.getInt( + context.contentResolver, + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, + 0 + ) == 1 + + @Suppress("DEPRECATION") + fun isCanInstallUnknownApps(context: Context) = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.INSTALL_NON_MARKET_APPS + ) == 1 + } else { + AppGlobal.packageManager.canRequestPackageInstalls() + } + + fun openInstallUnknownAppsScreen(context: Context) { + context.startActivity(Intent().apply { + action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + Settings.ACTION_SECURITY_SETTINGS + } else { + data = Uri.parse("package:${BuildConfig.APPLICATION_ID}") + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES + } + }) + } + + fun copyText(label: String? = "", text: String) { + AppGlobal.clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/ArrayUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/ArrayUtils.kt new file mode 100644 index 00000000..5310a154 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/ArrayUtils.kt @@ -0,0 +1,64 @@ +package ru.melod1n.project.vkm.util + +import ru.melod1n.project.vkm.extensions.ArrayExtensions.asArrayList +import java.util.stream.Collectors + +object ArrayUtils { + + @SafeVarargs + fun asString(vararg array: T): String { + if (array.isEmpty()) { + return "" + } + + val builder = StringBuilder(array.size * 12) + builder.append(array[0]) + for (i in 1 until array.size) { + builder.append(',') + builder.append(array[i]) + } + return builder.toString() + } + + fun asString(array: IntArray): String { + if (array.isEmpty()) { + return "" + } + + val builder = StringBuilder(array.size * 12) + builder.append(array[0]) + for (i in 1 until array.size) { + builder.append(',') + builder.append(array[i]) + } + return builder.toString() + } + + fun asString(arrayList: ArrayList): String { +// if (arrayList.isEmpty()) return "" +// +// val builder = StringBuilder(arrayList.size * 12) +// builder.append(arrayList[0]) +// for (i in 1 until arrayList.size) { +// builder.append(',') +// builder.append(arrayList[i]) +// } +// return builder.toString() + + return ArrayList().apply { + arrayList.forEach { add(it.toString()) } + }.stream().collect(Collectors.joining(",")) + } + + fun asString(list: List): String = asString(list.asArrayList()) + + fun cut(arrayList: ArrayList, offset: Int, count: Int): ArrayList { + if (arrayList.isEmpty()) return arrayListOf() + + var lastPosition = offset + count + if (lastPosition > arrayList.size) lastPosition = arrayList.size + + return ArrayList(arrayList.subList(offset, lastPosition)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/ImageUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/ImageUtils.kt new file mode 100644 index 00000000..1fb1c084 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/ImageUtils.kt @@ -0,0 +1,61 @@ +package ru.melod1n.project.vkm.util + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.widget.ImageView +import com.facebook.drawee.view.SimpleDraweeView +import com.squareup.picasso.Picasso +import com.squareup.picasso.Target + +object ImageUtils { + + fun loadImage(image: String, imageView: ImageView, placeholder: Drawable?) { + if (image.isEmpty()) return + + if (imageView is SimpleDraweeView) { + imageView.setImageURI(image) + return + } + + val picasso = Picasso.get() + .load(image) + .priority(Picasso.Priority.LOW) + + if (placeholder != null) picasso.placeholder(placeholder) + + picasso.into(imageView) + } + + fun loadImage(image: String?, listener: OnLoadListener?) { + if (image.isNullOrEmpty()) return + + val picasso = Picasso.get() + .load(image) + .priority(Picasso.Priority.LOW) + +// if (placeholder != null) picasso.placeholder(placeholder) + + + val target = object : Target { + override fun onPrepareLoad(placeHolderDrawable: Drawable?) { + + } + + override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) { + listener?.onError(e) + } + + override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { + listener?.onLoad(bitmap) + } + } + + picasso.into(target) + } + + + interface OnLoadListener { + fun onLoad(bitmap: Bitmap) + fun onError(e: Exception) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/KeyboardUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/KeyboardUtils.kt new file mode 100644 index 00000000..cd4df5ed --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/KeyboardUtils.kt @@ -0,0 +1,16 @@ +package ru.melod1n.project.vkm.util + +import android.view.View +import ru.melod1n.project.vkm.common.AppGlobal + +object KeyboardUtils { + + fun hideKeyboardFrom(view: View) { + AppGlobal.inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } + + fun showKeyboard(focusedView: View) { + AppGlobal.inputMethodManager.showSoftInput(focusedView, 0) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/TextUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/TextUtils.kt new file mode 100644 index 00000000..dd6959b1 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/TextUtils.kt @@ -0,0 +1,15 @@ +package ru.melod1n.project.vkm.util + +object TextUtils { + + fun getFirstLetterFromString(string: String): String { + for (i in string.indices) { + val char = string[i] + + if (char.isLetter()) return char.toString() + } + + return "" + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/TimeUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/TimeUtils.kt new file mode 100644 index 00000000..303f9a91 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/TimeUtils.kt @@ -0,0 +1,17 @@ +package ru.melod1n.project.vkm.util + +import java.util.* + +object TimeUtils { + + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/Utils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/Utils.kt new file mode 100644 index 00000000..10b7f6ae --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/Utils.kt @@ -0,0 +1,60 @@ +package ru.melod1n.project.vkm.util + +import android.content.Context +import android.content.res.Configuration +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.common.AppGlobal +import ru.melod1n.project.vkm.extensions.ArrayExtensions.isNullOrEmpty +import ru.melod1n.project.vkm.io.BytesOutputStream +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + +object Utils { + + fun getLocalizedThrowable(context: Context, t: Throwable): String { + return context.getString(R.string.error, t.message.toString()) + } + + fun isDarkTheme(): Boolean { + val currentNightMode = + AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return when (currentNightMode) { + Configuration.UI_MODE_NIGHT_YES -> true + else -> false + } + } + + fun serialize(source: Any?): ByteArray? { + try { + val bos = BytesOutputStream() + val out = ObjectOutputStream(bos) + out.writeObject(source) + out.close() + return bos.byteArray + } catch (e: IOException) { + e.printStackTrace() + } + return null + } + + fun deserialize(source: ByteArray?): Any? { + if (source.isNullOrEmpty()) { + return null + } + + try { + val bis = ByteArrayInputStream(source) + val `in` = ObjectInputStream(bis) + val o = `in`.readObject() + `in`.close() + return o + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/util/ViewUtils.kt b/app/src/main/java/ru/melod1n/project/vkm/util/ViewUtils.kt new file mode 100644 index 00000000..a4cd75f8 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/util/ViewUtils.kt @@ -0,0 +1,56 @@ +package ru.melod1n.project.vkm.util + +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.text.TextUtils +import android.view.View +import android.widget.TextView +import android.widget.Toast +import com.google.android.material.snackbar.Snackbar +import com.squareup.picasso.Picasso +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.api.model.VKUser +import ru.melod1n.project.vkm.api.util.VKUtil +import ru.melod1n.project.vkm.extensions.ContextExtensions.color +import ru.melod1n.project.vkm.widget.CircleImageView + + +object ViewUtils { + + fun showErrorSnackbar(view: View, t: Throwable) { + Snackbar.make( + view, + Utils.getLocalizedThrowable(view.context, t), + Snackbar.LENGTH_LONG + ).show() + } + + fun showErrorToast(context: Context, t: Throwable) { + Toast.makeText( + context, + Utils.getLocalizedThrowable(context, t), + Toast.LENGTH_LONG + ).show() + } + + fun prepareNavigationHeader(view: View, user: VKUser) { + val profileName = view.findViewById(R.id.headerName) + + profileName.text = user.toString() + + val profileStatus = view.findViewById(R.id.headerStatus) + + val statusText = if (TextUtils.isEmpty(user.status)) "@id${user.userId}" else user.status + + profileStatus.text = statusText + + val profileAvatar: CircleImageView = view.findViewById(R.id.headerAvatar) + + if (AndroidUtils.hasConnection()) { + Picasso.get().load(VKUtil.getUserPhoto(user)).into(profileAvatar) + } else { + profileAvatar.setImageDrawable(ColorDrawable(view.context.color(R.color.accent))) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/BoundedLinearLayout.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/BoundedLinearLayout.kt new file mode 100644 index 00000000..f473c96c --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/BoundedLinearLayout.kt @@ -0,0 +1,61 @@ +package ru.melod1n.project.vkm.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import ru.melod1n.project.vkm.R + +class BoundedLinearLayout : LinearLayout { + private var mBoundedWidth: Int + private var mBoundedHeight: Int + + constructor(context: Context?) : super(context) { + mBoundedWidth = 0 + mBoundedHeight = 0 + } + + @SuppressLint("CustomViewStyleable") + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + val a = context.obtainStyledAttributes(attrs, R.styleable.BoundedView) + mBoundedWidth = a.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0) + mBoundedHeight = a.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0) + a.recycle() + } + + var maxWidth: Int + get() = mBoundedWidth + set(width) { + if (mBoundedWidth != width) { + mBoundedWidth = width + requestLayout() + } + } + + var maxHeight: Int + get() = mBoundedHeight + set(height) { + if (mBoundedHeight != height) { + mBoundedHeight = height + requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // Adjust width as necessary + var widthMeasureSpec = widthMeasureSpec + var heightMeasureSpec = heightMeasureSpec + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + if (mBoundedWidth in 1 until measuredWidth) { + val measureMode = MeasureSpec.getMode(widthMeasureSpec) + widthMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedWidth, measureMode) + } + // Adjust height as necessary + val measuredHeight = MeasureSpec.getSize(heightMeasureSpec) + if (mBoundedHeight in 1 until measuredHeight) { + val measureMode = MeasureSpec.getMode(heightMeasureSpec) + heightMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedHeight, measureMode) + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/CircleImageView.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/CircleImageView.kt new file mode 100644 index 00000000..f5b7f0f7 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/CircleImageView.kt @@ -0,0 +1,59 @@ +package ru.melod1n.project.vkm.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.graphics.RectF +import android.util.AttributeSet +import android.view.ViewTreeObserver +import androidx.appcompat.widget.AppCompatImageView + +class CircleImageView : AppCompatImageView { + + companion object { + val SCALE_TYPE = ScaleType.CENTER_CROP + } + + private var path: Path? = null + private var rect: RectF? = null + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + + init() + } + + + override fun onDraw(canvas: Canvas?) { + rect ?: return + canvas ?: return + + if (rect!!.right == 0f || rect!!.bottom == 0f) { + createRect(width, height) + } + + canvas.clipPath(path!!) + super.onDraw(canvas) + } + + private fun init() { + scaleType = SCALE_TYPE + + viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + createRect(width, height) + viewTreeObserver.removeOnPreDrawListener(this) + return false + } + }) + } + + private fun createRect(width: Int, height: Int) { + rect = RectF(0f, 0f, width.toFloat(), height.toFloat()) + path = Path() + path!!.addRoundRect(rect!!, (width / 2).toFloat(), (height / 2).toFloat(), Path.Direction.CW) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/NoItemsView.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/NoItemsView.kt new file mode 100644 index 00000000..8101e977 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/NoItemsView.kt @@ -0,0 +1,150 @@ +package ru.melod1n.project.vkm.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import ru.melod1n.project.vkm.R +import ru.melod1n.project.vkm.base.BaseAdapter +import ru.melod1n.project.vkm.extensions.ContextExtensions.drawable +import ru.melod1n.project.vkm.extensions.DrawableExtensions.tint +import ru.melod1n.project.vkm.extensions.FloatExtensions.int +import ru.melod1n.project.vkm.util.AndroidUtils + +@Suppress("UNCHECKED_CAST") +class NoItemsView @JvmOverloads constructor( + context: Context, private var attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private lateinit var noItemsPicture: ImageView + private lateinit var noItemsTextView: TextView + + private var recyclerView: RecyclerView? = null + + private val textViewParams + get() = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ) + + private val imageViewParams + get() = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ) + + init { + create() + } + + private fun create() { + val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView) + + minimumWidth = AndroidUtils.px(256).int() + minimumHeight = minimumWidth + + orientation = VERTICAL + gravity = Gravity.CENTER + + noItemsPicture = ImageView(context) + + val params = imageViewParams + params.height = AndroidUtils.px(64).int() + params.width = AndroidUtils.px(64).int() + + noItemsPicture.layoutParams = params + + val noItemsDrawable = a.getDrawable(R.styleable.NoItemsView_noItemsImage) + noItemsDrawable?.let { + val noItemsDrawableTintColor = a.getColor(R.styleable.NoItemsView_noItemsImageTint, -1) + if (noItemsDrawableTintColor != -1) { + it.setTint(noItemsDrawableTintColor) + } + + setNoItemsImage(it) + } + + addView(noItemsPicture) + + noItemsTextView = TextView(context) + + val textParams = textViewParams + textParams.width = AndroidUtils.px(256).int() + + if (noItemsDrawable != null) { + textParams.topMargin = AndroidUtils.px(8).int() + } + + noItemsTextView.layoutParams = textParams + + noItemsTextView.gravity = Gravity.CENTER + noItemsTextView.setTextAppearance(R.style.TextAppearance_MaterialComponents_Body1) + + val noItemsTextColor = a.getColor(R.styleable.NoItemsView_noItemsTextColor, -1) + if (noItemsTextColor != -1) { + setNoItemsTextColor(noItemsTextColor) + } + + val noItemsText = a.getString(R.styleable.NoItemsView_noItemsText) + noItemsText?.let { + setNoItemsText(it) + } + + addView(noItemsTextView) + + val isVisibleByDefault = a.getBoolean(R.styleable.NoItemsView_isVisibleByDefault, true) + isVisible = isVisibleByDefault + + a.recycle() + } + + fun setNoItemsImage(@DrawableRes resId: Int) { + setNoItemsImage(context.drawable(resId)) + } + + fun setNoItemsImage(drawable: Drawable?) { + noItemsPicture.setImageDrawable(drawable) + } + + fun setNoItemsImageTint(@ColorInt color: Int) { + noItemsPicture.drawable.tint(color) + } + + fun setNoItemsText(@StringRes resId: Int) { + noItemsTextView.setText(resId) + } + + fun setNoItemsText(text: String) { + noItemsTextView.text = text + } + + fun setNoItemsTextColor(@ColorInt color: Int) { + noItemsTextView.setTextColor(color) + } + + fun syncWith(recyclerView: RecyclerView) { + this.recyclerView = recyclerView + + recyclerView.noItemsView = this + } + + fun > updateState(adapter: ADAPTER) { + if (adapter.isEmpty()) show() else hide() + } + + fun show() { + isVisible = true + } + + fun hide() { + isVisible = false + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/RecyclerView.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/RecyclerView.kt new file mode 100644 index 00000000..ca182f00 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/RecyclerView.kt @@ -0,0 +1,18 @@ +package ru.melod1n.project.vkm.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.RecyclerView + +class RecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + + var noItemsView: NoItemsView? = null + + + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/Toolbar.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/Toolbar.kt new file mode 100644 index 00000000..4ae32824 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/Toolbar.kt @@ -0,0 +1,132 @@ +package ru.melod1n.project.vkm.widget + +import android.app.Activity +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import com.facebook.drawee.view.SimpleDraweeView +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.theme.overlay.MaterialThemeOverlay +import ru.melod1n.project.vkm.R + +class Toolbar : MaterialToolbar { + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + MaterialThemeOverlay.wrap( + context, + attrs, + defStyleAttr, + com.google.android.material.R.style.Widget_MaterialComponents_Toolbar + ), + attrs, + defStyleAttr + ) + + private fun init() { + //... + } + + override fun setTitle(resId: Int) { + title = context.getString(resId) + } + + override fun setTitle(title: CharSequence?) { + findViewById(R.id.toolbarTitle).text = title + } + + override fun setTitleTextColor(color: Int) { + findViewById(R.id.toolbarTitle).setTextColor(color) + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + init() + } + + override fun setNavigationIcon(icon: Drawable?) { + findViewById(R.id.toolbarNavigationIcon).setImageDrawable(icon) + } + + override fun setNavigationIcon(@DrawableRes resId: Int) { + findViewById(R.id.toolbarNavigationIcon).setImageResource(resId) + } + +// fun setNavigationIconTintList(tintList: ColorStateList?) { +// findViewById(R.id.toolbarNavigationIcon).drawable?.setTintList(tintList) +// } +// +// fun setNavigationIconTint(@ColorInt tintColor: Int) { +// findViewById(R.id.toolbarNavigationIcon).drawable?.setTint(tintColor) +// } + + fun setNavigationClickListener(listener: OnClickListener?) { + findViewById(R.id.toolbarNavigation).setOnClickListener(listener) + } + + fun setNavigationOnBackClickListener(activity: Activity) { + findViewById(R.id.toolbarNavigation).setOnClickListener { activity.onBackPressed() } + } + + fun setNavigationVisibility(visible: Boolean) { + findViewById(R.id.toolbarNavigation).visibility = if (visible) VISIBLE else GONE + } + + fun setAvatarIcon(icon: Drawable?) { + findViewById(R.id.toolbarAvatar).setImageDrawable(icon) + } + + fun setAvatarIcon(@DrawableRes resId: Int) { + findViewById(R.id.toolbarAvatar).setActualImageResource(resId) + } + + fun setAvatarClickListener(listener: OnClickListener?) { + findViewById(R.id.toolbarAvatar).setOnClickListener(listener) + } + + fun setAvatarVisibility(visible: Boolean) { + findViewById(R.id.toolbarAvatar).visibility = if (visible) VISIBLE else GONE + } + + fun getAvatar(): SimpleDraweeView { + return findViewById(R.id.toolbarAvatar) + } + + fun setTitleMode(titleMode: TitleMode) { + val title = findViewById(R.id.toolbarTitle) + + when (titleMode) { + TitleMode.SIMPLE -> { + title.gravity = Gravity.CENTER + title.typeface = ResourcesCompat.getFont(context, R.font.google_sans_medium) + title.setTextColor(ContextCompat.getColor(context, R.color.accent)) + title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f) + } + TitleMode.HINT -> { + title.gravity = Gravity.CENTER_VERTICAL and Gravity.START + title.typeface = ResourcesCompat.getFont(context, R.font.google_sans_regular) + title.setTextColor(ContextCompat.getColor(context, R.color.text_secondary_60_alpha)) + title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + } + } + } + + enum class TitleMode { + SIMPLE, HINT + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/melod1n/project/vkm/widget/WrapTextView.kt b/app/src/main/java/ru/melod1n/project/vkm/widget/WrapTextView.kt new file mode 100644 index 00000000..47b12516 --- /dev/null +++ b/app/src/main/java/ru/melod1n/project/vkm/widget/WrapTextView.kt @@ -0,0 +1,56 @@ +package ru.melod1n.project.vkm.widget + +import android.content.Context +import android.text.Layout +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import ru.melod1n.project.vkm.R +import kotlin.math.ceil +import kotlin.math.max + +class WrapTextView(context: Context, attrs: AttributeSet? = null) : + AppCompatTextView(context, attrs) { + + private var fixWrapText = false + + constructor(context: Context) : this(context, null) + + init { + init(context, attrs) + } + + private fun init(context: Context, attrs: AttributeSet?) { + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.WrapTextView, 0, 0) + + try { + fixWrapText = a.getBoolean(R.styleable.WrapTextView_fixWrap, false) + } finally { + a.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + if (fixWrapText && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { + val width = getMaxWidth(layout) + if (width in 1 until measuredWidth) { + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), + heightMeasureSpec + ) + } + } + } + + private fun getMaxWidth(layout: Layout): Int { + if (layout.lineCount < 2) return 0 + + var maxWidth = 0.0f + for (i in 0 until layout.lineCount) { + maxWidth = max(maxWidth, layout.getLineWidth(i)) + } + + return ceil(maxWidth).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_enter.xml b/app/src/main/res/anim/activity_close_enter.xml new file mode 100644 index 00000000..2651d5c9 --- /dev/null +++ b/app/src/main/res/anim/activity_close_enter.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_exit.xml b/app/src/main/res/anim/activity_close_exit.xml new file mode 100644 index 00000000..a16e9e5f --- /dev/null +++ b/app/src/main/res/anim/activity_close_exit.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/anim/activity_open_enter.xml b/app/src/main/res/anim/activity_open_enter.xml new file mode 100644 index 00000000..46e1a064 --- /dev/null +++ b/app/src/main/res/anim/activity_open_enter.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/anim/activity_open_exit.xml b/app/src/main/res/anim/activity_open_exit.xml new file mode 100644 index 00000000..faf8690c --- /dev/null +++ b/app/src/main/res/anim/activity_open_exit.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fast_out_extra_slow_in.xml b/app/src/main/res/anim/fast_out_extra_slow_in.xml new file mode 100644 index 00000000..241111b6 --- /dev/null +++ b/app/src/main/res/anim/fast_out_extra_slow_in.xml @@ -0,0 +1,3 @@ + + diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml new file mode 100644 index 00000000..4e536ca3 --- /dev/null +++ b/app/src/main/res/anim/slide_down.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml new file mode 100644 index 00000000..35b523d8 --- /dev/null +++ b/app/src/main/res/anim/slide_up.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-nodpi/face.jpg b/app/src/main/res/drawable-nodpi/face.jpg new file mode 100644 index 00000000..05000a14 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/face.jpg differ diff --git a/app/src/main/res/drawable-v21/ic_account_circle_outline.xml b/app/src/main/res/drawable-v21/ic_account_circle_outline.xml new file mode 100644 index 00000000..6e430808 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_account_circle_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_arrow_back.xml b/app/src/main/res/drawable-v21/ic_arrow_back.xml new file mode 100644 index 00000000..71d5bbd2 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_arrow_end.xml b/app/src/main/res/drawable-v21/ic_arrow_end.xml new file mode 100644 index 00000000..1b975c48 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_arrow_end.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_close.xml b/app/src/main/res/drawable-v21/ic_close.xml new file mode 100644 index 00000000..c8204159 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_dialog_type_channel.xml b/app/src/main/res/drawable-v21/ic_dialog_type_channel.xml new file mode 100644 index 00000000..35d1f13b --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_dialog_type_channel.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_dialog_type_conversation.xml b/app/src/main/res/drawable-v21/ic_dialog_type_conversation.xml new file mode 100644 index 00000000..03ce6730 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_dialog_type_conversation.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_done.xml b/app/src/main/res/drawable-v21/ic_done.xml new file mode 100644 index 00000000..8b757ba7 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_done.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_edit.xml b/app/src/main/res/drawable-v21/ic_edit.xml new file mode 100644 index 00000000..2173930d --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_edit.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_email.xml b/app/src/main/res/drawable-v21/ic_email.xml new file mode 100644 index 00000000..462d9124 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_email.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_error.xml b/app/src/main/res/drawable-v21/ic_error.xml new file mode 100644 index 00000000..7ff1ad7a --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_error.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/ic_exit_to_app.xml b/app/src/main/res/drawable-v21/ic_exit_to_app.xml new file mode 100644 index 00000000..e233f593 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_exit_to_app.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_fast_lightning.xml b/app/src/main/res/drawable-v21/ic_fast_lightning.xml new file mode 100644 index 00000000..8fc57eb9 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_fast_lightning.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/ic_file_download.xml b/app/src/main/res/drawable-v21/ic_file_download.xml new file mode 100644 index 00000000..a243261e --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_file_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_format_size.xml b/app/src/main/res/drawable-v21/ic_format_size.xml new file mode 100644 index 00000000..e64060ed --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_format_size.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_info_outline.xml b/app/src/main/res/drawable-v21/ic_info_outline.xml new file mode 100644 index 00000000..af0d4d06 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_info_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_key.xml b/app/src/main/res/drawable-v21/ic_key.xml new file mode 100644 index 00000000..581f83e0 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_key.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_lock.xml b/app/src/main/res/drawable-v21/ic_lock.xml new file mode 100644 index 00000000..58daccc5 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_lock_outline.xml b/app/src/main/res/drawable-v21/ic_lock_outline.xml new file mode 100644 index 00000000..e876745f --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_lock_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_menu.xml b/app/src/main/res/drawable-v21/ic_menu.xml new file mode 100644 index 00000000..1cad3ddf --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_audio.xml b/app/src/main/res/drawable-v21/ic_message_attachment_audio.xml new file mode 100644 index 00000000..c565ad12 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_audio.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_audio_message.xml b/app/src/main/res/drawable-v21/ic_message_attachment_audio_message.xml new file mode 100644 index 00000000..848de634 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_audio_message.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_call.xml b/app/src/main/res/drawable-v21/ic_message_attachment_call.xml new file mode 100644 index 00000000..d7d4a4e3 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_call.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_camera.xml b/app/src/main/res/drawable-v21/ic_message_attachment_camera.xml new file mode 100644 index 00000000..33d3947a --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_camera.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_doc.xml b/app/src/main/res/drawable-v21/ic_message_attachment_doc.xml new file mode 100644 index 00000000..253e03d4 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_doc.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_gift.xml b/app/src/main/res/drawable-v21/ic_message_attachment_gift.xml new file mode 100644 index 00000000..080918cd --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_gift.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_graffiti.xml b/app/src/main/res/drawable-v21/ic_message_attachment_graffiti.xml new file mode 100644 index 00000000..d237c079 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_graffiti.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_link.xml b/app/src/main/res/drawable-v21/ic_message_attachment_link.xml new file mode 100644 index 00000000..3766173c --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_poll.xml b/app/src/main/res/drawable-v21/ic_message_attachment_poll.xml new file mode 100644 index 00000000..322dd6fa --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_poll.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_sticker.xml b/app/src/main/res/drawable-v21/ic_message_attachment_sticker.xml new file mode 100644 index 00000000..212d6e8b --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_sticker.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_message_attachment_video.xml b/app/src/main/res/drawable-v21/ic_message_attachment_video.xml new file mode 100644 index 00000000..c1e846be --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_attachment_video.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_message_outline.xml b/app/src/main/res/drawable-v21/ic_message_outline.xml new file mode 100644 index 00000000..dbb492ab --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_message_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_mic.xml b/app/src/main/res/drawable-v21/ic_mic.xml new file mode 100644 index 00000000..b1ea93c9 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_mic.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_no_internet.xml b/app/src/main/res/drawable-v21/ic_no_internet.xml new file mode 100644 index 00000000..6f31b586 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_no_internet.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/ic_no_items.xml b/app/src/main/res/drawable-v21/ic_no_items.xml new file mode 100644 index 00000000..2c25c5f7 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_no_items.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/ic_online_mobile.xml b/app/src/main/res/drawable-v21/ic_online_mobile.xml new file mode 100644 index 00000000..72249755 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_online_mobile.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable-v21/ic_online_pc.xml b/app/src/main/res/drawable-v21/ic_online_pc.xml new file mode 100644 index 00000000..e6968b24 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_online_pc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_outline_format_paint.xml b/app/src/main/res/drawable-v21/ic_outline_format_paint.xml new file mode 100644 index 00000000..a1bbe431 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_outline_format_paint.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_outline_menu_24.xml b/app/src/main/res/drawable-v21/ic_outline_menu_24.xml new file mode 100644 index 00000000..470db520 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_outline_menu_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_palette_swatch_outline.xml b/app/src/main/res/drawable-v21/ic_palette_swatch_outline.xml new file mode 100644 index 00000000..11bc5a0a --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_palette_swatch_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_people_outline.xml b/app/src/main/res/drawable-v21/ic_people_outline.xml new file mode 100644 index 00000000..2f8cbd63 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_people_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_phone_android.xml b/app/src/main/res/drawable-v21/ic_phone_android.xml new file mode 100644 index 00000000..97509f3f --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_phone_android.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_refresh.xml b/app/src/main/res/drawable-v21/ic_refresh.xml new file mode 100644 index 00000000..9e4f8dfc --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_search.xml b/app/src/main/res/drawable-v21/ic_search.xml new file mode 100644 index 00000000..be5ad99c --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_send.xml b/app/src/main/res/drawable-v21/ic_send.xml new file mode 100644 index 00000000..742495e6 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_send.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_settings.xml b/app/src/main/res/drawable-v21/ic_settings.xml new file mode 100644 index 00000000..833f05cf --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_settings_outline.xml b/app/src/main/res/drawable-v21/ic_settings_outline.xml new file mode 100644 index 00000000..1f264d7f --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_settings_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_star_border.xml b/app/src/main/res/drawable-v21/ic_star_border.xml new file mode 100644 index 00000000..3fc251d1 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_star_border.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_start_bottom.xml b/app/src/main/res/drawable-v21/ic_start_bottom.xml new file mode 100644 index 00000000..b82b2d49 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_start_bottom.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_system_update.xml b/app/src/main/res/drawable-v21/ic_system_update.xml new file mode 100644 index 00000000..900763c8 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_system_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_trash_outline.xml b/app/src/main/res/drawable-v21/ic_trash_outline.xml new file mode 100644 index 00000000..a465f3c6 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_trash_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_vkm.xml b/app/src/main/res/drawable-v21/ic_vkm.xml new file mode 100644 index 00000000..2505b97f --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_vkm.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_start_screen_background.png b/app/src/main/res/drawable-xhdpi/ic_start_screen_background.png new file mode 100644 index 00000000..0b046380 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_start_screen_background.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_start_screen_background.png b/app/src/main/res/drawable-xxhdpi/ic_start_screen_background.png new file mode 100644 index 00000000..53bdf266 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_start_screen_background.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_start_screen_background.png b/app/src/main/res/drawable-xxxhdpi/ic_start_screen_background.png new file mode 100644 index 00000000..aab60e8a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_start_screen_background.png differ diff --git a/app/src/main/res/drawable/chat_panel_background.xml b/app/src/main/res/drawable/chat_panel_background.xml new file mode 100644 index 00000000..35dd6dcf --- /dev/null +++ b/app/src/main/res/drawable/chat_panel_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_panel_background_blocked.xml b/app/src/main/res/drawable/chat_panel_background_blocked.xml new file mode 100644 index 00000000..ad8234b6 --- /dev/null +++ b/app/src/main/res/drawable/chat_panel_background_blocked.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_conversations_counter_background.xml b/app/src/main/res/drawable/ic_conversations_counter_background.xml new file mode 100644 index 00000000..a28ba1db --- /dev/null +++ b/app/src/main/res/drawable/ic_conversations_counter_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..a74b6bd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_login_edittext_background.xml b/app/src/main/res/drawable/ic_login_edittext_background.xml new file mode 100644 index 00000000..3aa66b03 --- /dev/null +++ b/app/src/main/res/drawable/ic_login_edittext_background.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logo_fast_border.xml b/app/src/main/res/drawable/ic_logo_fast_border.xml new file mode 100644 index 00000000..5e5d690a --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_fast_border.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_bubble_in_simple.xml b/app/src/main/res/drawable/ic_message_bubble_in_simple.xml new file mode 100644 index 00000000..a3c8ca6b --- /dev/null +++ b/app/src/main/res/drawable/ic_message_bubble_in_simple.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_bubble_out_simple.xml b/app/src/main/res/drawable/ic_message_bubble_out_simple.xml new file mode 100644 index 00000000..bb2dae4f --- /dev/null +++ b/app/src/main/res/drawable/ic_message_bubble_out_simple.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_timestamp.xml b/app/src/main/res/drawable/ic_message_timestamp.xml new file mode 100644 index 00000000..a5f04c5c --- /dev/null +++ b/app/src/main/res/drawable/ic_message_timestamp.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outline_bug_report_24.xml b/app/src/main/res/drawable/ic_outline_bug_report_24.xml new file mode 100644 index 00000000..5d3845da --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_bug_report_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/navigation_view_item_background_active.xml b/app/src/main/res/drawable/navigation_view_item_background_active.xml new file mode 100644 index 00000000..329088d9 --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_item_background_active.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/navigation_view_item_background_normal.xml b/app/src/main/res/drawable/navigation_view_item_background_normal.xml new file mode 100644 index 00000000..31529d0f --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_item_background_normal.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/navigation_view_item_background_selector.xml b/app/src/main/res/drawable/navigation_view_item_background_selector.xml new file mode 100644 index 00000000..6dc493b9 --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_item_background_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/navigation_view_item_icon_colors.xml b/app/src/main/res/drawable/navigation_view_item_icon_colors.xml new file mode 100644 index 00000000..5b3daf0c --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_item_icon_colors.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/navigation_view_item_text_colors.xml b/app/src/main/res/drawable/navigation_view_item_text_colors.xml new file mode 100644 index 00000000..7b504faf --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_item_text_colors.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/navigation_view_items_colors.xml b/app/src/main/res/drawable/navigation_view_items_colors.xml new file mode 100644 index 00000000..2c6b906e --- /dev/null +++ b/app/src/main/res/drawable/navigation_view_items_colors.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toolbar_background.xml b/app/src/main/res/drawable/toolbar_background.xml new file mode 100644 index 00000000..7ec92e07 --- /dev/null +++ b/app/src/main/res/drawable/toolbar_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toolbar_background_ripple.xml b/app/src/main/res/drawable/toolbar_background_ripple.xml new file mode 100644 index 00000000..9ce8eb1b --- /dev/null +++ b/app/src/main/res/drawable/toolbar_background_ripple.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/google_sans_bold.ttf b/app/src/main/res/font/google_sans_bold.ttf new file mode 100644 index 00000000..065c1542 Binary files /dev/null and b/app/src/main/res/font/google_sans_bold.ttf differ diff --git a/app/src/main/res/font/google_sans_italic.ttf b/app/src/main/res/font/google_sans_italic.ttf new file mode 100644 index 00000000..1a3d405a Binary files /dev/null and b/app/src/main/res/font/google_sans_italic.ttf differ diff --git a/app/src/main/res/font/google_sans_medium.ttf b/app/src/main/res/font/google_sans_medium.ttf new file mode 100644 index 00000000..81b8714a Binary files /dev/null and b/app/src/main/res/font/google_sans_medium.ttf differ diff --git a/app/src/main/res/font/google_sans_regular.ttf b/app/src/main/res/font/google_sans_regular.ttf new file mode 100644 index 00000000..e017417b Binary files /dev/null and b/app/src/main/res/font/google_sans_regular.ttf differ diff --git a/app/src/main/res/font/roboto_bold.ttf b/app/src/main/res/font/roboto_bold.ttf new file mode 100644 index 00000000..50fe0db7 Binary files /dev/null and b/app/src/main/res/font/roboto_bold.ttf differ diff --git a/app/src/main/res/font/roboto_light.ttf b/app/src/main/res/font/roboto_light.ttf new file mode 100644 index 00000000..04cc9550 Binary files /dev/null and b/app/src/main/res/font/roboto_light.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 00000000..0c1a577b Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 00000000..f16ad7af Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/font/roboto_thin.ttf b/app/src/main/res/font/roboto_thin.ttf new file mode 100644 index 00000000..d728fa9b Binary files /dev/null and b/app/src/main/res/font/roboto_thin.ttf differ diff --git a/app/src/main/res/font/tt_commons_bold.ttf b/app/src/main/res/font/tt_commons_bold.ttf new file mode 100644 index 00000000..98aa0411 Binary files /dev/null and b/app/src/main/res/font/tt_commons_bold.ttf differ diff --git a/app/src/main/res/font/tt_commons_medium.ttf b/app/src/main/res/font/tt_commons_medium.ttf new file mode 100644 index 00000000..730b1fa1 Binary files /dev/null and b/app/src/main/res/font/tt_commons_medium.ttf differ diff --git a/app/src/main/res/font/tt_commons_regular.ttf b/app/src/main/res/font/tt_commons_regular.ttf new file mode 100644 index 00000000..545705cf Binary files /dev/null and b/app/src/main/res/font/tt_commons_regular.ttf differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 00000000..64508c8d --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login_custom_data.xml b/app/src/main/res/layout/activity_login_custom_data.xml new file mode 100644 index 00000000..4b937509 --- /dev/null +++ b/app/src/main/res/layout/activity_login_custom_data.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..856d5f1c --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_drawer_header.xml b/app/src/main/res/layout/activity_main_drawer_header.xml new file mode 100644 index 00000000..b913023c --- /dev/null +++ b/app/src/main/res/layout/activity_main_drawer_header.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_messages.xml b/app/src/main/res/layout/activity_messages.xml new file mode 100644 index 00000000..81e47094 --- /dev/null +++ b/app/src/main/res/layout/activity_messages.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 00000000..348f426b --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_start.xml b/app/src/main/res/layout/activity_start.xml new file mode 100644 index 00000000..894bbd20 --- /dev/null +++ b/app/src/main/res/layout/activity_start.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_update.xml b/app/src/main/res/layout/activity_update.xml new file mode 100644 index 00000000..dfdca701 --- /dev/null +++ b/app/src/main/res/layout/activity_update.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_account.xml b/app/src/main/res/layout/dialog_account.xml new file mode 100644 index 00000000..445cd9b4 --- /dev/null +++ b/app/src/main/res/layout/dialog_account.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_profile_bottom.xml b/app/src/main/res/layout/dialog_profile_bottom.xml new file mode 100644 index 00000000..16223db8 --- /dev/null +++ b/app/src/main/res/layout/dialog_profile_bottom.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_view.xml b/app/src/main/res/layout/error_view.xml new file mode 100644 index 00000000..d685a0fe --- /dev/null +++ b/app/src/main/res/layout/error_view.xml @@ -0,0 +1,50 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_conversations.xml b/app/src/main/res/layout/fragment_conversations.xml new file mode 100644 index 00000000..6e43092c --- /dev/null +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_friends.xml b/app/src/main/res/layout/fragment_friends.xml new file mode 100644 index 00000000..31acca80 --- /dev/null +++ b/app/src/main/res/layout/fragment_friends.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_important.xml b/app/src/main/res/layout/fragment_important.xml new file mode 100644 index 00000000..7fd71ed8 --- /dev/null +++ b/app/src/main/res/layout/fragment_important.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 00000000..aa59a474 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml new file mode 100644 index 00000000..ff4aacb9 --- /dev/null +++ b/app/src/main/res/layout/item_conversation.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_conversation_light.xml b/app/src/main/res/layout/item_conversation_light.xml new file mode 100644 index 00000000..0afb05fe --- /dev/null +++ b/app/src/main/res/layout/item_conversation_light.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml new file mode 100644 index 00000000..c8b42af5 --- /dev/null +++ b/app/src/main/res/layout/item_message.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_action.xml b/app/src/main/res/layout/item_message_action.xml new file mode 100644 index 00000000..f188c91a --- /dev/null +++ b/app/src/main/res/layout/item_message_action.xml @@ -0,0 +1,14 @@ + + diff --git a/app/src/main/res/layout/item_message_attachment_in.xml b/app/src/main/res/layout/item_message_attachment_in.xml new file mode 100644 index 00000000..cc179056 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_in.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_out.xml b/app/src/main/res/layout/item_message_attachment_out.xml new file mode 100644 index 00000000..ff7fe586 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_out.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_channel.xml b/app/src/main/res/layout/item_message_channel.xml new file mode 100644 index 00000000..e1f5af2d --- /dev/null +++ b/app/src/main/res/layout/item_message_channel.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_normal_in.xml b/app/src/main/res/layout/item_message_normal_in.xml new file mode 100644 index 00000000..d335c1de --- /dev/null +++ b/app/src/main/res/layout/item_message_normal_in.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_normal_out.xml b/app/src/main/res/layout/item_message_normal_out.xml new file mode 100644 index 00000000..d9dcaa2e --- /dev/null +++ b/app/src/main/res/layout/item_message_normal_out.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_timestamp.xml b/app/src/main/res/layout/item_message_timestamp.xml new file mode 100644 index 00000000..e264d1da --- /dev/null +++ b/app/src/main/res/layout/item_message_timestamp.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_simple_menu.xml b/app/src/main/res/layout/item_simple_menu.xml new file mode 100644 index 00000000..cb8de4a7 --- /dev/null +++ b/app/src/main/res/layout/item_simple_menu.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_user.xml b/app/src/main/res/layout/item_user.xml new file mode 100644 index 00000000..b15fc9c4 --- /dev/null +++ b/app/src/main/res/layout/item_user.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/no_internet_view.xml b/app/src/main/res/layout/no_internet_view.xml new file mode 100644 index 00000000..8883a21a --- /dev/null +++ b/app/src/main/res/layout/no_internet_view.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/no_items_view.xml b/app/src/main/res/layout/no_items_view.xml new file mode 100644 index 00000000..06aefad1 --- /dev/null +++ b/app/src/main/res/layout/no_items_view.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_view.xml b/app/src/main/res/layout/recycler_view.xml new file mode 100644 index 00000000..1295140e --- /dev/null +++ b/app/src/main/res/layout/recycler_view.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar.xml b/app/src/main/res/layout/toolbar.xml new file mode 100644 index 00000000..9dda06d4 --- /dev/null +++ b/app/src/main/res/layout/toolbar.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_floating.xml b/app/src/main/res/layout/toolbar_floating.xml new file mode 100644 index 00000000..9eeda9a4 --- /dev/null +++ b/app/src/main/res/layout/toolbar_floating.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_bottom.xml b/app/src/main/res/menu/activity_main_bottom.xml new file mode 100644 index 00000000..4e42f714 --- /dev/null +++ b/app/src/main/res/menu/activity_main_bottom.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml new file mode 100644 index 00000000..0d29edef --- /dev/null +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_messages.xml b/app/src/main/res/menu/activity_messages.xml new file mode 100644 index 00000000..6f72c973 --- /dev/null +++ b/app/src/main/res/menu/activity_messages.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/empty.xml b/app/src/main/res/menu/empty.xml new file mode 100644 index 00000000..f156b75b --- /dev/null +++ b/app/src/main/res/menu/empty.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml new file mode 100644 index 00000000..ca900c3c --- /dev/null +++ b/app/src/main/res/menu/fragment_conversations.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_friends.xml b/app/src/main/res/menu/fragment_friends.xml new file mode 100644 index 00000000..fe187c0c --- /dev/null +++ b/app/src/main/res/menu/fragment_friends.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..2d95290f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..bdd6be92 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..2d95290f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..7aa4db3a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2eb4ff41 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..7aa4db3a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..03a242be Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..30b88aa3 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..03a242be Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..13ceda1b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..b07c785e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..13ceda1b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..19b32ab5 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..99a34029 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..19b32ab5 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..ce9709d6 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,10 @@ + + + #000000 + #000000 + #63ACFF + #000000 + #000000 + + #292929 + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..25fe7ea9 --- /dev/null +++ b/app/src/main/res/values-night/styles.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..eef503b9 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml new file mode 100644 index 00000000..8ccb65c9 --- /dev/null +++ b/app/src/main/res/values-v27/themes.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..b71d6560 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,42 @@ + + + + + @string/message_attachment_audio + @string/message_attachment_doc + @string/message_attachment_gift + @string/message_attachment_graffiti + @string/message_attachment_link + @string/message_attachment_photo + @string/message_attachment_poll + @string/message_attachment_sticker + @string/message_attachment_video + @string/message_attachment_voice + + + + Minimal + Normal + Extended + + + + 0 + 1 + 2 + + + + @string/theme_light + @string/theme_dark + @string/theme_power_saving + @string/theme_system + + + + 0 + 0 + 0 + 0 + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..d78d4d7f --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..949e9bc4 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,28 @@ + + + #ffffff + #ffffff + #4284F4 + #E4E4E4 + #ffffff + + @color/accent + + #e6e6e6 + + #ff000000 + #DE000000 + #99000000 + + #ffffff + #DEFFFFFF + #99FFFFFF + + #666666 + + #F5F5F5 + + #5c5c5c + #e8e8e8 + #ffffff + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..fe991af4 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + 16dp + 16dp + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..70a24a75 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #4284F4 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..c3a8cd36 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,155 @@ + + Fast + Fast Messenger + + Created chat %s + Invited %s to chat + Kicked %s from chat + %s Returned to chat + %s Left from chat + %s Updated chat photo + %s Removed chat photo + %s Updated chat title + %s Pinned message + %s Unpinned message + %s Invited by link + + Forwarded message + %d Forwarded messages + + Attachments + Photo + Video + Audio + Document + Link + Sticker + Gift + Graffiti + Voice message + Poll + Call + + %d Photos + %d Videos + %d Audios + %d Documents + + Login + + Search + Conversations + Friends + Important + Settings + + Yesterday + Name Surname + Status + Logout + + now + %ds + %dm + %dh + %dd + %dmo + %dy + Application version + Tap to check updates + Application chiefs and helpers + About + Account + Type… + Token + Custom data + User Id + Appearance + Conversations type + Loading… + + Online + Online from mobile + Offline + Last seen at %s + Last seen recently + + %d members + + Channel • %d members + Press & hold to record voice + No connection + Update + Project.VKM + Enter + No access + + Dialog is empty + List is empty + + Downloading… + Checking… + No updates + Current version: %s (%d) + Last update check in %s + Changelog: %s + Check updates + Update is available + New version: %s (%d) + Download + Refresh + + Today + Login settings + In progress + + Yesterday + + Refresh + + Edited + + No conversations + No friends :( + + General + Hide the keyboard while scrolling up + App has been crashed + + Light + Dark + By power saving + By system + Theme + Send report + OK + + @null + Clear conversations cache + Search + An error has occurred + Account + Debug + Clear users and groups cache + Error + Error loading message. Try again? + Retry + + Error: %s + + I can\'t see anything… + Oops… + There is something unexpected… + + Warning + Yes + No + Installing apk\'s is disabled in settings. Open settings screen to enable it? + Password + E-mail or phone number + Log in + Captcha + Input code from picture + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..d2801a3a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml new file mode 100644 index 00000000..f4822042 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings_about.xml b/app/src/main/res/xml/fragment_settings_about.xml new file mode 100644 index 00000000..02cdb701 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings_about.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings_account.xml b/app/src/main/res/xml/fragment_settings_account.xml new file mode 100644 index 00000000..c4576be2 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings_account.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings_appearance.xml b/app/src/main/res/xml/fragment_settings_appearance.xml new file mode 100644 index 00000000..e53cb814 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings_appearance.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings_debug.xml b/app/src/main/res/xml/fragment_settings_debug.xml new file mode 100644 index 00000000..0cd1f045 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings_debug.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_settings_general.xml b/app/src/main/res/xml/fragment_settings_general.xml new file mode 100644 index 00000000..1bf518f9 --- /dev/null +++ b/app/src/main/res/xml/fragment_settings_general.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..5b9dd9fb --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..997de5b7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,22 @@ +buildscript { + ext.kotlin_version = "1.4.30" + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..98bed167 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a6db7c56 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Feb 14 01:38:52 MSK 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..fc346764 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "Fast" \ No newline at end of file