Lot of global changes (#10)

Global update
This commit is contained in:
2022-08-30 19:49:52 +03:00
committed by GitHub
parent 8d0cd19322
commit 7a99347841
230 changed files with 9172 additions and 3157 deletions
+92 -62
View File
@@ -1,12 +1,17 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import com.android.build.gradle.internal.api.BaseVariantOutputImpl
val login: String = gradleLocalProperties(rootDir).getProperty("vkLogin")
val password: String = gradleLocalProperties(rootDir).getProperty("vkPassword")
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage") val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage")
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint") val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint")
val msAppCenterToken: String =
gradleLocalProperties(rootDir).getProperty("msAppCenterAppToken", null)
val otaSecretCode: String = gradleLocalProperties(rootDir).getProperty("otaSecretCode")
val majorVersion = 1
val minorVersion = 6
val patchVersion = 4
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -16,15 +21,23 @@ plugins {
} }
android { android {
compileSdk = 31 namespace = "com.meloda.fast"
buildToolsVersion = "31.0.0"
compileSdk = 32
applicationVariants.all {
outputs.all {
(this as BaseVariantOutputImpl).outputFileName =
"${name}-${versionName}-${versionCode}.apk"
}
}
defaultConfig { defaultConfig {
applicationId = "com.meloda.fast" applicationId = "com.meloda.fast"
minSdk = 23 minSdk = 23
targetSdk = 30 targetSdk = 32
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "alpha"
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
@@ -35,24 +48,27 @@ android {
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password)
buildConfigField("String", "sdkPackage", sdkPackage) buildConfigField("String", "sdkPackage", sdkPackage)
buildConfigField("String", "sdkFingerprint", sdkFingerprint) buildConfigField("String", "sdkFingerprint", sdkFingerprint)
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
buildConfigField("String", "otaSecretCode", otaSecretCode)
versionNameSuffix = "_${getVersionName()}"
} }
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password)
buildConfigField("String", "sdkPackage", sdkPackage) buildConfigField("String", "sdkPackage", sdkPackage)
buildConfigField("String", "sdkFingerprint", sdkFingerprint) buildConfigField("String", "sdkFingerprint", sdkFingerprint)
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
buildConfigField("String", "otaSecretCode", otaSecretCode)
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
"proguard-rules.pro"
) )
} }
} }
@@ -62,74 +78,88 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions {
freeCompilerArgs = listOf("-Xjvm-default=compatibility", "-opt-in=kotlin.RequiresOptIn")
}
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
} }
kapt { fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
correctErrorTypes = true
//use this shit if you don't want have hilt errors val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
javacOptions {
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
}
}
dependencies { dependencies {
// Cicerone - Navigation implementation(kotlin("reflect", "1.6.10"))
implementation("com.github.terrakok:cicerone:7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.3") implementation(libs.androidx.core)
implementation("com.github.massoudss:waveformSeekBar:3.1.0") implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation("androidx.core:core-splashscreen:1.0.0-beta02") implementation(libs.androidx.splashScreen)
implementation("androidx.work:work-runtime-ktx:2.7.1") implementation(libs.androidx.dataStore)
implementation("androidx.datastore:datastore-preferences:1.0.0") implementation(libs.androidx.appCompat)
implementation("androidx.paging:paging-runtime-ktx:3.1.1") implementation(libs.androidx.activity)
implementation("androidx.appcompat:appcompat:1.4.1") implementation(libs.androidx.fragment)
implementation("com.google.android.material:material:1.6.0-beta01")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.fragment:fragment-ktx:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") implementation(libs.androidx.preference)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation("androidx.room:room-ktx:2.4.2") implementation(libs.androidx.swipeRefreshLayout)
implementation("androidx.room:room-runtime:2.4.2")
kapt("androidx.room:room-compiler:2.4.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1") implementation(libs.androidx.recyclerView)
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.1")
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.1")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") implementation(libs.androidx.cardView)
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.dagger:hilt-android:2.39.1") implementation(libs.androidx.constraintLayout)
kapt("com.google.dagger:hilt-android-compiler:2.39.1")
implementation("com.github.yogacp:android-viewbinding:1.0.4") implementation(libs.androidx.room)
implementation(libs.androidx.room.runtime)
kapt(libs.androidx.room.compiler)
implementation("io.coil-kt:coil:1.4.0") implementation(libs.cicerone)
implementation("com.google.code.gson:gson:2.8.8") implementation(libs.waveformSeekBar)
implementation("org.jsoup:jsoup:1.14.3")
implementation("ch.acra:acra:4.11.1")
implementation("com.github.bumptech.glide:glide:4.13.0") implementation(libs.glide)
kapt("com.github.bumptech.glide:compiler:4.13.0") kapt(libs.glide.compiler)
implementation(libs.kPermissions)
implementation(libs.kPermissions.coroutines)
implementation(libs.appCenter.analytics)
implementation(libs.appCenter.crashes)
implementation(libs.hilt)
kapt(libs.hilt.compiler)
implementation(libs.retrofit)
implementation(libs.retrofit.gson.converter)
implementation(libs.okhttp3)
implementation(libs.okhttp3.interceptor)
implementation(libs.coroutines.core)
implementation(libs.coroutines.android)
implementation(libs.viewBindingDelegate)
implementation(libs.google.gson)
implementation(libs.google.guava)
implementation(libs.google.material)
implementation(libs.jsoup)
implementation(libs.chucker)
} }
@@ -0,0 +1,582 @@
{
"formatVersion": 1,
"database": {
"version": 33,
"identityHash": "ab075cc511743c47de441d484159b088",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, PRIMARY KEY(`userId`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fastToken",
"columnName": "fastToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"userId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo200` TEXT, `type` TEXT NOT NULL, `callInProgress` INTEGER NOT NULL, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `isMarkedUnread` INTEGER NOT NULL, `lastMessageId` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessage_id` INTEGER, `pinnedMessage_text` TEXT, `pinnedMessage_isOut` INTEGER, `pinnedMessage_peerId` INTEGER, `pinnedMessage_fromId` INTEGER, `pinnedMessage_date` INTEGER, `pinnedMessage_randomId` INTEGER, `pinnedMessage_action` TEXT, `pinnedMessage_actionMemberId` INTEGER, `pinnedMessage_actionText` TEXT, `pinnedMessage_actionConversationMessageId` INTEGER, `pinnedMessage_actionMessage` TEXT, `pinnedMessage_important` INTEGER, `pinnedMessage_forwards` TEXT, `pinnedMessage_attachments` TEXT, `pinnedMessage_replyMessage` TEXT, `pinnedMessage_geo` TEXT, `lastMessage_id` INTEGER, `lastMessage_text` TEXT, `lastMessage_isOut` INTEGER, `lastMessage_peerId` INTEGER, `lastMessage_fromId` INTEGER, `lastMessage_date` INTEGER, `lastMessage_randomId` INTEGER, `lastMessage_action` TEXT, `lastMessage_actionMemberId` INTEGER, `lastMessage_actionText` TEXT, `lastMessage_actionConversationMessageId` INTEGER, `lastMessage_actionMessage` TEXT, `lastMessage_important` INTEGER, `lastMessage_forwards` TEXT, `lastMessage_attachments` TEXT, `lastMessage_replyMessage` TEXT, `lastMessage_geo` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callInProgress",
"columnName": "callInProgress",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastConversationMessageId",
"columnName": "lastConversationMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isMarkedUnread",
"columnName": "isMarkedUnread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessage.id",
"columnName": "pinnedMessage_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.text",
"columnName": "pinnedMessage_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.isOut",
"columnName": "pinnedMessage_isOut",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.peerId",
"columnName": "pinnedMessage_peerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.fromId",
"columnName": "pinnedMessage_fromId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.date",
"columnName": "pinnedMessage_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.randomId",
"columnName": "pinnedMessage_randomId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.action",
"columnName": "pinnedMessage_action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionMemberId",
"columnName": "pinnedMessage_actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionText",
"columnName": "pinnedMessage_actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionConversationMessageId",
"columnName": "pinnedMessage_actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionMessage",
"columnName": "pinnedMessage_actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.important",
"columnName": "pinnedMessage_important",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.forwards",
"columnName": "pinnedMessage_forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.attachments",
"columnName": "pinnedMessage_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.replyMessage",
"columnName": "pinnedMessage_replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.geo",
"columnName": "pinnedMessage_geo",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.id",
"columnName": "lastMessage_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.text",
"columnName": "lastMessage_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.isOut",
"columnName": "lastMessage_isOut",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.peerId",
"columnName": "lastMessage_peerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.fromId",
"columnName": "lastMessage_fromId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.date",
"columnName": "lastMessage_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.randomId",
"columnName": "lastMessage_randomId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.action",
"columnName": "lastMessage_action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.actionMemberId",
"columnName": "lastMessage_actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.actionText",
"columnName": "lastMessage_actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.actionConversationMessageId",
"columnName": "lastMessage_actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.actionMessage",
"columnName": "lastMessage_actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.important",
"columnName": "lastMessage_important",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.forwards",
"columnName": "lastMessage_forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.attachments",
"columnName": "lastMessage_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.replyMessage",
"columnName": "lastMessage_replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.geo",
"columnName": "lastMessage_geo",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `important` INTEGER NOT NULL, `forwards` TEXT, `attachments` TEXT, `replyMessage` TEXT, `geo` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actionConversationMessageId",
"columnName": "actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwards",
"columnName": "forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyMessage",
"columnName": "replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "geo",
"columnName": "geo",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `online` INTEGER NOT NULL, `photo200` TEXT, `lastSeen` INTEGER, `lastSeenStatus` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "online",
"columnName": "online",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab075cc511743c47de441d484159b088')"
]
}
}
@@ -0,0 +1,600 @@
{
"formatVersion": 1,
"database": {
"version": 34,
"identityHash": "2c202b1fce1b5f6c6ab0da756e0590a6",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, PRIMARY KEY(`userId`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fastToken",
"columnName": "fastToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"userId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo200` TEXT, `type` TEXT NOT NULL, `callInProgress` INTEGER NOT NULL, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `isMarkedUnread` INTEGER NOT NULL, `lastMessageId` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessage_id` INTEGER, `pinnedMessage_text` TEXT, `pinnedMessage_isOut` INTEGER, `pinnedMessage_peerId` INTEGER, `pinnedMessage_fromId` INTEGER, `pinnedMessage_date` INTEGER, `pinnedMessage_randomId` INTEGER, `pinnedMessage_action` TEXT, `pinnedMessage_actionMemberId` INTEGER, `pinnedMessage_actionText` TEXT, `pinnedMessage_actionConversationMessageId` INTEGER, `pinnedMessage_actionMessage` TEXT, `pinnedMessage_updateTime` INTEGER, `pinnedMessage_important` INTEGER, `pinnedMessage_forwards` TEXT, `pinnedMessage_attachments` TEXT, `pinnedMessage_replyMessage` TEXT, `pinnedMessage_geo` TEXT, `lastMessage_id` INTEGER, `lastMessage_text` TEXT, `lastMessage_isOut` INTEGER, `lastMessage_peerId` INTEGER, `lastMessage_fromId` INTEGER, `lastMessage_date` INTEGER, `lastMessage_randomId` INTEGER, `lastMessage_action` TEXT, `lastMessage_actionMemberId` INTEGER, `lastMessage_actionText` TEXT, `lastMessage_actionConversationMessageId` INTEGER, `lastMessage_actionMessage` TEXT, `lastMessage_updateTime` INTEGER, `lastMessage_important` INTEGER, `lastMessage_forwards` TEXT, `lastMessage_attachments` TEXT, `lastMessage_replyMessage` TEXT, `lastMessage_geo` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callInProgress",
"columnName": "callInProgress",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastConversationMessageId",
"columnName": "lastConversationMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isMarkedUnread",
"columnName": "isMarkedUnread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessage.id",
"columnName": "pinnedMessage_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.text",
"columnName": "pinnedMessage_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.isOut",
"columnName": "pinnedMessage_isOut",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.peerId",
"columnName": "pinnedMessage_peerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.fromId",
"columnName": "pinnedMessage_fromId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.date",
"columnName": "pinnedMessage_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.randomId",
"columnName": "pinnedMessage_randomId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.action",
"columnName": "pinnedMessage_action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionMemberId",
"columnName": "pinnedMessage_actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionText",
"columnName": "pinnedMessage_actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionConversationMessageId",
"columnName": "pinnedMessage_actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.actionMessage",
"columnName": "pinnedMessage_actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.updateTime",
"columnName": "pinnedMessage_updateTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.important",
"columnName": "pinnedMessage_important",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinnedMessage.forwards",
"columnName": "pinnedMessage_forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.attachments",
"columnName": "pinnedMessage_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.replyMessage",
"columnName": "pinnedMessage_replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pinnedMessage.geo",
"columnName": "pinnedMessage_geo",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.id",
"columnName": "lastMessage_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.text",
"columnName": "lastMessage_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.isOut",
"columnName": "lastMessage_isOut",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.peerId",
"columnName": "lastMessage_peerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.fromId",
"columnName": "lastMessage_fromId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.date",
"columnName": "lastMessage_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.randomId",
"columnName": "lastMessage_randomId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.action",
"columnName": "lastMessage_action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.actionMemberId",
"columnName": "lastMessage_actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.actionText",
"columnName": "lastMessage_actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.actionConversationMessageId",
"columnName": "lastMessage_actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.actionMessage",
"columnName": "lastMessage_actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.updateTime",
"columnName": "lastMessage_updateTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.important",
"columnName": "lastMessage_important",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessage.forwards",
"columnName": "lastMessage_forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.attachments",
"columnName": "lastMessage_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.replyMessage",
"columnName": "lastMessage_replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessage.geo",
"columnName": "lastMessage_geo",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwards` TEXT, `attachments` TEXT, `replyMessage` TEXT, `geo` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "actionConversationMessageId",
"columnName": "actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwards",
"columnName": "forwards",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyMessage",
"columnName": "replyMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "geo",
"columnName": "geo",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `online` INTEGER NOT NULL, `photo200` TEXT, `lastSeen` INTEGER, `lastSeenStatus` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "online",
"columnName": "online",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2c202b1fce1b5f6c6ab0da756e0590a6')"
]
}
}
+22 -5
View File
@@ -1,10 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.meloda.fast">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application <application
android:name=".common.AppGlobal" android:name=".common.AppGlobal"
@@ -16,11 +24,15 @@
android:supportsRtl="true" android:supportsRtl="true"
android:testOnly="false" android:testOnly="false"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:allowBackup"> android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:ignore="DataExtractionRules">
<activity <activity
android:name=".activity.MainActivity" android:name=".screens.main.MainActivity"
android:theme="@style/AppTheme.Splash"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -29,7 +41,12 @@
</activity> </activity>
<service <service
android:name=".service.MessagesUpdateService" android:name=".service.LongPollService"
android:enabled="true"
android:exported="false" />
<service
android:name=".service.OnlineService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
@@ -1,54 +0,0 @@
package com.meloda.fast.activity
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Router
import com.github.terrakok.cicerone.androidx.AppNavigator
import com.github.terrakok.cicerone.androidx.FragmentScreen
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.Screens
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : BaseActivity(R.layout.activity_main) {
private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {
override fun setupFragmentTransaction(
screen: FragmentScreen,
fragmentTransaction: FragmentTransaction,
currentFragment: Fragment?,
nextFragment: Fragment
) {
// fragmentTransaction.setCustomAnimations(
// R.anim.activity_open_enter, R.anim.activity_close_exit,
// R.anim.activity_close_enter, R.anim.activity_open_exit
// )
}
}
@Inject
lateinit var navigatorHolder: NavigatorHolder
@Inject
lateinit var router: Router
override fun onResumeFragments() {
navigatorHolder.setNavigator(navigator)
super.onResumeFragments()
}
override fun onPause() {
navigatorHolder.removeNavigator()
super.onPause()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
router.newRootScreen(Screens.Main())
}
}
@@ -1,24 +1,24 @@
package com.meloda.fast.api package com.meloda.fast.api
enum class ApiEvent(val value: Int) { enum class ApiEvent(val value: Int) {
MESSAGE_SET_FLAGS(2), MessageSetFlags(2),
MESSAGE_CLEAR_FLAGS(3), MessageClearFlags(3),
MESSAGE_NEW(4), MessageNew(4),
MESSAGE_EDIT(5), MessageEdit(5),
MESSAGE_READ_INCOMING(6), MessageReadIncoming(6),
MESSAGE_READ_OUTGOING(7), MessageReadOutgoing(7),
FRIEND_ONLINE(8), FriendOnline(8),
FRIEND_OFFLINE(9), FriendOffline(9),
MESSAGES_DELETED(13), MessagesDeleted(13),
PIN_UNPIN_CONVERSATION(20), PinUnpinConversation(20),
PRIVATE_TYPING(61), PrivateTyping(61),
CHAT_TYPING(62), ChatTyping(62),
ONE_MORE_TYPING(63), OneMoreTyping(63),
VOICE_RECORDING(64), VoiceRecording(64),
PHOTO_UPLOADING(65), PhotoUploading(65),
VIDEO_UPLOADING(66), VideoUploading(66),
FILE_UPLOADING(67), FileUploading(67),
UNREAD_COUNT_UPDATE(80) UnreadCountUpdate(80)
; ;
companion object { companion object {
@@ -1,45 +1,46 @@
package com.meloda.fast.api package com.meloda.fast.api
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.model.AppAccount
object UserConfig { object UserConfig {
private const val FAST_TOKEN = "fast_token" private const val ARG_CURRENT_USER_ID = "current_user_id"
private const val TOKEN = "token"
private const val USER_ID = "user_id"
const val FAST_APP_ID = "6964679" const val FAST_APP_ID = "6964679"
private val preferences get() = AppGlobal.preferences
var currentUserId: Int = -1
get() = preferences.getInt(ARG_CURRENT_USER_ID, -1)
set(value) {
field = value
preferences.edit { putInt(ARG_CURRENT_USER_ID, value) }
}
var userId: Int = -1 var userId: Int = -1
get() = AppGlobal.preferences.getInt(USER_ID, -1)
set(value) {
field = value
AppGlobal.preferences.edit().putInt(USER_ID, value).apply()
}
var accessToken: String = "" var accessToken: String = ""
get() = AppGlobal.preferences.getString(TOKEN, "") ?: "" var fastToken: String? = ""
set(value) {
field = value
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
}
var fastToken: String = "" fun parse(account: AppAccount) {
get() = AppGlobal.preferences.getString(FAST_TOKEN, "") ?: "" this.userId = account.userId
set(value) { this.accessToken = account.accessToken
field = value this.fastToken = account.fastToken
AppGlobal.preferences.edit().putString(FAST_TOKEN, value).apply()
} }
fun clear() { fun clear() {
currentUserId = -1
accessToken = "" accessToken = ""
fastToken = "" fastToken = ""
userId = -1 userId = -1
} }
fun isLoggedIn() = userId > 0 && accessToken.isNotBlank() fun isLoggedIn(): Boolean {
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
}
val vkUser = MutableLiveData<VkUser?>(null) val vkUser = MutableLiveData<VkUser?>(null)
@@ -12,7 +12,7 @@ object VKConstants {
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
const val API_VERSION = "5.132" const val API_VERSION = "5.189"
const val LP_VERSION = 10 const val LP_VERSION = 10
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
@@ -53,22 +53,4 @@ object VKConstants {
VkVoiceMessage::class.java, VkVoiceMessage::class.java,
VkWidget::class.java VkWidget::class.java
) )
val separatedFromTextAttachments = listOf<Class<out VkAttachment>>(
VkPhoto::class.java,
VkVideo::class.java,
VkSticker::class.java,
VkStory::class.java,
VkWidget::class.java,
VkGroupCall::class.java,
VkGroupCall::class.java,
VkCurator::class.java,
VkEvent::class.java,
VkGift::class.java,
VkGraffiti::class.java,
VkPoll::class.java,
VkWall::class.java,
VkWallReply::class.java,
VkLink::class.java
)
} }
@@ -1,20 +0,0 @@
package com.meloda.fast.api
import org.json.JSONObject
import java.io.IOException
open class VKException(
var url: String = "",
var code: Int = -1,
var description: String = "",
var error: String
) : IOException(description) {
// TODO: 10-Oct-21 remove this
var json: JSONObject? = null
override fun toString(): String {
return "error: $error; description: $description;"
}
}
@@ -6,7 +6,9 @@ import android.graphics.drawable.Drawable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
@@ -14,7 +16,10 @@ import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.* import com.meloda.fast.api.model.attachments.*
import com.meloda.fast.api.model.base.BaseVkMessage import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import com.meloda.fast.api.network.*
import com.meloda.fast.extensions.orDots
@Suppress("MemberVisibilityCanBePrivate")
object VkUtils { object VkUtils {
fun <T> attachmentToString( fun <T> attachmentToString(
@@ -44,12 +49,12 @@ object VkUtils {
fun getMessageUser(message: VkMessage, profiles: Map<Int, VkUser>): VkUser? { fun getMessageUser(message: VkMessage, profiles: Map<Int, VkUser>): VkUser? {
return (if (!message.isUser()) null return (if (!message.isUser()) null
else profiles[message.fromId]).also { message.user.value = it } else profiles[message.fromId]).also { message.user = it }
} }
fun getMessageGroup(message: VkMessage, groups: Map<Int, VkGroup>): VkGroup? { fun getMessageGroup(message: VkMessage, groups: Map<Int, VkGroup>): VkGroup? {
return (if (!message.isGroup()) null return (if (!message.isGroup()) null
else groups[message.fromId]).also { message.group.value = it } else groups[message.fromId]).also { message.group = it }
} }
fun getMessageAvatar( fun getMessageAvatar(
@@ -66,9 +71,19 @@ object VkUtils {
fun getMessageTitle( fun getMessageTitle(
message: VkMessage, message: VkMessage,
messageUser: VkUser?, defMessageUser: VkUser? = null,
messageGroup: VkGroup? defMessageGroup: VkGroup? = null,
profiles: Map<Int, VkUser>? = null,
groups: Map<Int, VkGroup>? = null
): String? { ): String? {
val messageUser: VkUser? =
defMessageUser ?: if (profiles == null) null
else profiles[message.fromId]
val messageGroup: VkGroup? =
defMessageGroup ?: if (groups == null) null
else groups[message.fromId]
return when { return when {
message.isUser() -> messageUser?.fullName message.isUser() -> messageUser?.fullName
message.isGroup() -> messageGroup?.name message.isGroup() -> messageGroup?.name
@@ -78,12 +93,12 @@ object VkUtils {
fun getConversationUser(conversation: VkConversation, profiles: Map<Int, VkUser>): VkUser? { fun getConversationUser(conversation: VkConversation, profiles: Map<Int, VkUser>): VkUser? {
return (if (!conversation.isUser()) null return (if (!conversation.isUser()) null
else profiles[conversation.id]).also { conversation.user.value = it } else profiles[conversation.id]).also { conversation.user.postValue(it) }
} }
fun getConversationGroup(conversation: VkConversation, groups: Map<Int, VkGroup>): VkGroup? { fun getConversationGroup(conversation: VkConversation, groups: Map<Int, VkGroup>): VkGroup? {
return (if (!conversation.isGroup()) null return (if (!conversation.isGroup()) null
else groups[conversation.id]).also { conversation.group.value = it } else groups[conversation.id]).also { conversation.group.postValue(it) }
} }
fun getConversationAvatar( fun getConversationAvatar(
@@ -92,7 +107,7 @@ object VkUtils {
conversationGroup: VkGroup? conversationGroup: VkGroup?
): String? { ): String? {
return when { return when {
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null conversation.isAccount() -> null
conversation.isUser() -> conversationUser?.photo200 conversation.isUser() -> conversationUser?.photo200
conversation.isGroup() -> conversationGroup?.photo200 conversation.isGroup() -> conversationGroup?.photo200
conversation.isChat() -> conversation.photo200 conversation.isChat() -> conversation.photo200
@@ -100,6 +115,53 @@ object VkUtils {
} }
} }
fun getConversationTitle(
context: Context,
conversation: VkConversation,
defConversationUser: VkUser? = null,
defConversationGroup: VkGroup? = null,
profiles: Map<Int, VkUser>? = null,
groups: Map<Int, VkGroup>? = null
): String? {
val conversationUser: VkUser? =
defConversationUser ?: if (profiles == null) null
else getConversationUser(conversation, profiles)
val conversationGroup: VkGroup? =
defConversationGroup ?: if (groups == null) null
else getConversationGroup(conversation, groups)
return when {
conversation.isAccount() -> context.getString(R.string.favorites)
conversation.isChat() -> conversation.title
conversation.isUser() -> conversationUser?.fullName
conversation.isGroup() -> conversationGroup?.name
else -> null
}
}
fun getConversationUserGroup(
conversation: VkConversation,
profiles: Map<Int, VkUser>,
groups: Map<Int, VkGroup>
): Pair<VkUser?, VkGroup?> {
val user: VkUser? = getConversationUser(conversation, profiles)
val group: VkGroup? = getConversationGroup(conversation, groups)
return user to group
}
fun getMessageUserGroup(
message: VkMessage,
profiles: Map<Int, VkUser>,
groups: Map<Int, VkGroup>
): Pair<VkUser?, VkGroup?> {
val user: VkUser? = getMessageUser(message, profiles)
val group: VkGroup? = getMessageGroup(message, groups)
return user to group
}
fun prepareMessageText(text: String, forConversations: Boolean? = null): String { fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
return text.apply { return text.apply {
if (forConversations == true) replace("\n", "") if (forConversations == true) replace("\n", "")
@@ -231,6 +293,7 @@ object VkUtils {
messageUser: VkUser? = null, messageUser: VkUser? = null,
messageGroup: VkGroup? = null messageGroup: VkGroup? = null
): SpannableString? { ): SpannableString? {
@Suppress("REDUNDANT_ELSE_IN_WHEN")
return when (message.getPreparedAction()) { return when (message.getPreparedAction()) {
VkMessage.Action.CHAT_CREATE -> { VkMessage.Action.CHAT_CREATE -> {
val text = message.actionText ?: return null val text = message.actionText ?: return null
@@ -245,12 +308,14 @@ object VkUtils {
val spanText = val spanText =
context.getString(R.string.message_action_chat_created, prefix, text) context.getString(R.string.message_action_chat_created, prefix, text)
val startIndex = spanText.indexOf(text, startIndex = prefix.length)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan( it.setSpan(
StyleSpan(Typeface.BOLD), StyleSpan(Typeface.BOLD),
spanText.indexOf(text, startIndex = prefix.length), startIndex,
text.length, 0 startIndex + text.length, 0
) )
} }
} }
@@ -329,7 +394,7 @@ object VkUtils {
} else { } else {
val prefix = val prefix =
if (message.fromId == UserConfig.userId) youPrefix if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..." else messageUser?.toString() ?: messageGroup?.toString().orDots()
val postfix = val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase() if (memberId == UserConfig.userId) youPrefix.lowercase()
@@ -374,7 +439,7 @@ object VkUtils {
} }
} else { } else {
val prefix = if (message.fromId == UserConfig.userId) youPrefix val prefix = if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..." else messageUser?.toString() ?: messageGroup?.toString().orDots()
val postfix = val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase() if (memberId == UserConfig.userId) youPrefix.lowercase()
@@ -410,6 +475,20 @@ object VkUtils {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
} }
VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText =
context.getString(R.string.message_action_chat_user_joined_by_call, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
val prefix = when { val prefix = when {
message.fromId == UserConfig.userId -> youPrefix message.fromId == UserConfig.userId -> youPrefix
@@ -520,8 +599,8 @@ object VkUtils {
} }
fun getAttachmentText(context: Context, message: VkMessage): String? { fun getAttachmentText(context: Context, message: VkMessage): String? {
message.geoType?.let { message.geo?.let {
return when (it) { return when (it.type) {
"point" -> context.getString(R.string.message_geo_point) "point" -> context.getString(R.string.message_geo_point)
else -> context.getString(R.string.message_geo) else -> context.getString(R.string.message_geo)
} }
@@ -551,14 +630,14 @@ object VkUtils {
} }
fun getAttachmentConversationIcon(context: Context, message: VkMessage): Drawable? { fun getAttachmentConversationIcon(context: Context, message: VkMessage): Drawable? {
message.geoType?.let { return message.attachments?.let { attachments ->
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
message.geo?.let {
return ContextCompat.getDrawable(context, R.drawable.ic_map_marker) return ContextCompat.getDrawable(context, R.drawable.ic_map_marker)
} }
if (message.attachments.isNullOrEmpty()) return null if (attachments.isEmpty()) return null
return message.attachments?.let { attachments ->
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
getAttachmentTypeByClass(attachments[0])?.let { getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentIconByType( getAttachmentIconByType(
context, context,
@@ -683,4 +762,37 @@ object VkUtils {
else -> attachmentType.value else -> attachmentType.value
} }
} }
fun getApiError(gson: Gson, errorString: String?): ApiAnswer.Error {
try {
val defaultError = gson.fromJson(errorString, ApiError::class.java)
val error: ApiError =
when (defaultError.error) {
VkErrorCodes.UserAuthorizationFailed.toString() -> {
val authorizationError =
gson.fromJson(errorString, AuthorizationError::class.java)
authorizationError
}
VkErrors.NeedValidation -> {
val validationError =
gson.fromJson(errorString, ValidationRequiredError::class.java)
validationError
}
VkErrors.NeedCaptcha -> {
val captchaRequiredError =
gson.fromJson(errorString, CaptchaRequiredError::class.java)
captchaRequiredError
}
else -> defaultError
}
return ApiAnswer.Error(error)
} catch (e: Exception) {
return ApiAnswer.Error(ApiError(throwable = e))
}
}
} }
@@ -1,11 +1,18 @@
package com.meloda.fast.api.base package com.meloda.fast.api.base
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.VKException import okio.IOException
data class ApiError( open class ApiError(
@SerializedName("error_code") @SerializedName("error", alternate = ["error_code"])
val errorCode: Int, val error: String? = null,
@SerializedName("error_msg") @SerializedName("error_msg", alternate = ["error_description"])
override var message: String open val errorMessage: String? = null,
) : VKException(error = message, code = errorCode) val throwable: Throwable? = null
) : IOException() {
override fun toString(): String {
return Gson().toJson(this)
}
}
@@ -1,4 +1,4 @@
package com.meloda.fast.api package com.meloda.fast.api.longpoll
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
@@ -1,31 +1,27 @@
package com.meloda.fast.api package com.meloda.fast.api.longpoll
import android.util.Log import android.util.Log
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.meloda.fast.api.ApiEvent
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.messages.MessagesDataSource
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
import com.meloda.fast.base.viewmodel.VkEventCallback import com.meloda.fast.base.viewmodel.VkEventCallback
import com.meloda.fast.data.messages.MessagesRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
class LongPollUpdatesParser( class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope {
private val messagesDataSource: MessagesDataSource
) : CoroutineScope {
companion object {
private const val TAG = "LongPollUpdatesParser"
}
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "error: $throwable") Log.d("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace() throwable.printStackTrace()
} }
@@ -36,51 +32,51 @@ class LongPollUpdatesParser(
mutableMapOf() mutableMapOf()
fun parseNextUpdate(event: JsonArray) { fun parseNextUpdate(event: JsonArray) {
val eventType: ApiEvent? = val eventId = event[0].asInt
try { val eventType: ApiEvent? = ApiEvent.parse(eventId)
ApiEvent.parse(event[0].asInt)
} catch (e: Exception) {
null
}
if (eventType != null) { if (eventType == null) {
println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
} else { return
println("$TAG: unknown event: $event")
} }
when (eventType) { when (eventType) {
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event)
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event)
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event) ApiEvent.MessageNew -> parseMessageNew(eventType, event)
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event) ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event) ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event) ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
ApiEvent.FRIEND_ONLINE -> parseFriendOnline(eventType, event) ApiEvent.FriendOnline -> parseFriendOnline(eventType, event)
ApiEvent.FRIEND_OFFLINE -> parseFriendOffline(eventType, event) ApiEvent.FriendOffline -> parseFriendOffline(eventType, event)
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event) ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
// ApiEvent.PIN_UNPIN_CONVERSATION -> TODO() ApiEvent.PinUnpinConversation -> onNewEvent(eventType, event)
// ApiEvent.TYPING -> TODO() ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
// ApiEvent.VOICE_RECORDING -> TODO() ApiEvent.ChatTyping -> onNewEvent(eventType, event)
// ApiEvent.PHOTO_UPLOADING -> TODO() ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
// ApiEvent.VIDEO_UPLOADING -> TODO() ApiEvent.VoiceRecording -> onNewEvent(eventType, event)
// ApiEvent.FILE_UPLOADING -> TODO() ApiEvent.PhotoUploading -> onNewEvent(eventType, event)
// ApiEvent.UNREAD_COUNT_UPDATE -> TODO() ApiEvent.VideoUploading -> onNewEvent(eventType, event)
ApiEvent.FileUploading -> onNewEvent(eventType, event)
ApiEvent.UnreadCountUpdate -> onNewEvent(eventType, event)
} }
} }
private fun onNewEvent(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) { private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
} }
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) { private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
} }
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) { private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt val messageId = event[1].asInt
launch { launch {
@@ -90,7 +86,7 @@ class LongPollUpdatesParser(
messageId messageId
) )
listenersMap[ApiEvent.MESSAGE_NEW]?.let { listenersMap[ApiEvent.MessageNew]?.let {
it.map { vkEventCallback -> it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>) (vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
.onEvent(newMessageEvent) .onEvent(newMessageEvent)
@@ -100,8 +96,7 @@ class LongPollUpdatesParser(
} }
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) { private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt val messageId = event[1].asInt
launch { launch {
@@ -111,7 +106,7 @@ class LongPollUpdatesParser(
messageId messageId
) )
listenersMap[ApiEvent.MESSAGE_EDIT]?.let { listenersMap[ApiEvent.MessageEdit]?.let {
it.map { vkEventCallback -> it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>) (vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
.onEvent(editedMessageEvent) .onEvent(editedMessageEvent)
@@ -121,11 +116,12 @@ class LongPollUpdatesParser(
} }
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) { private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt val peerId = event[1].asInt
val messageId = event[2].asInt val messageId = event[2].asInt
launch { launch {
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
listeners.map { vkEventCallback -> listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) (vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
.onEvent( .onEvent(
@@ -140,11 +136,12 @@ class LongPollUpdatesParser(
} }
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) { private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt val peerId = event[1].asInt
val messageId = event[2].asInt val messageId = event[2].asInt
launch { launch {
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
listeners.map { vkEventCallback -> listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) (vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
.onEvent( .onEvent(
@@ -159,22 +156,22 @@ class LongPollUpdatesParser(
} }
private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) { private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
} }
private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) { private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
} }
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) { private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
// println("$TAG: $eventType: $event") Log.d("LongPollUpdatesParser", "$eventType: $event")
} }
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) = private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
coroutineScope { coroutineScope {
suspendCoroutine<T> { suspendCoroutine<T> {
launch { launch {
val normalMessageResponse = messagesDataSource.getById( val normalMessageResponse = messagesRepository.getById(
MessagesGetByIdRequest( MessagesGetByIdRequest(
messagesIds = listOf(messageId), messagesIds = listOf(messageId),
extended = true, extended = true,
@@ -182,17 +179,19 @@ class LongPollUpdatesParser(
) )
) )
if (normalMessageResponse !is Answer.Success) { if (!normalMessageResponse.isSuccessful()) {
(normalMessageResponse as Answer.Error).throwable.let { throw it } normalMessageResponse.error.throwable?.run { throw this }
} }
val messagesResponse = normalMessageResponse.data.response ?: return@launch val messagesResponse =
(normalMessageResponse as? ApiAnswer.Success)?.data?.response
?: return@launch
val messagesList = messagesResponse.items val messagesList = messagesResponse.items
if (messagesList.isEmpty()) return@launch if (messagesList.isEmpty()) return@launch
val normalMessage = messagesList[0].asVkMessage() val normalMessage = messagesList[0].asVkMessage()
messagesDataSource.store(listOf(normalMessage)) messagesRepository.store(listOf(normalMessage))
val profiles = hashMapOf<Int, VkUser>() val profiles = hashMapOf<Int, VkUser>()
messagesResponse.profiles?.forEach { baseUser -> messagesResponse.profiles?.forEach { baseUser ->
@@ -205,13 +204,13 @@ class LongPollUpdatesParser(
} }
val resumeValue: LongPollEvent? = when (eventType) { val resumeValue: LongPollEvent? = when (eventType) {
ApiEvent.MESSAGE_NEW -> ApiEvent.MessageNew ->
LongPollEvent.VkMessageNewEvent( LongPollEvent.VkMessageNewEvent(
normalMessage, normalMessage,
profiles, profiles,
groups groups
) )
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(normalMessage) ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage)
else -> null else -> null
} }
@@ -221,7 +220,7 @@ class LongPollUpdatesParser(
} }
fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) { private fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
listenersMap.let { map -> listenersMap.let { map ->
map[eventType] = (map[eventType] ?: mutableListOf()).also { map[eventType] = (map[eventType] ?: mutableListOf()).also {
it.add(listener) it.add(listener)
@@ -230,7 +229,7 @@ class LongPollUpdatesParser(
} }
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) { fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener) registerListener(ApiEvent.MessageReadIncoming, listener)
} }
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
@@ -238,7 +237,7 @@ class LongPollUpdatesParser(
} }
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) { fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener) registerListener(ApiEvent.MessageReadOutgoing, listener)
} }
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
@@ -246,7 +245,7 @@ class LongPollUpdatesParser(
} }
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) { fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
registerListener(ApiEvent.MESSAGE_NEW, listener) registerListener(ApiEvent.MessageNew, listener)
} }
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
@@ -254,7 +253,7 @@ class LongPollUpdatesParser(
} }
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) { fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
registerListener(ApiEvent.MESSAGE_EDIT, listener) registerListener(ApiEvent.MessageEdit, listener)
} }
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
@@ -266,9 +265,7 @@ class LongPollUpdatesParser(
} }
} }
internal inline fun <R : Any> assembleEventCallback( internal inline fun <R : Any> assembleEventCallback(crossinline block: (R) -> Unit): VkEventCallback<R> {
crossinline block: (R) -> Unit
): VkEventCallback<R> {
return object : VkEventCallback<R> { return object : VkEventCallback<R> {
override fun onEvent(event: R) = block.invoke(event) override fun onEvent(event: R) = block.invoke(event)
} }
@@ -5,6 +5,7 @@ import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.model.SelectableItem import com.meloda.fast.model.SelectableItem
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -25,10 +26,11 @@ data class VkConversation(
var outRead: Int, var outRead: Int,
var isMarkedUnread: Boolean, var isMarkedUnread: Boolean,
var lastMessageId: Int, var lastMessageId: Int,
var unreadCount: Int?, var unreadCount: Int,
var membersCount: Int?, var membersCount: Int?,
var isPinned: Boolean,
var canChangePin: Boolean, var canChangePin: Boolean,
var majorId: Int,
var minorId: Int,
@Embedded(prefix = "pinnedMessage_") @Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null, var pinnedMessage: VkMessage? = null,
@@ -49,9 +51,13 @@ data class VkConversation(
fun isUser() = type == "user" fun isUser() = type == "user"
fun isGroup() = type == "group" fun isGroup() = type == "group"
fun isInUnread() = inRead < lastMessageId fun isInUnread() = inRead - lastMessageId < 0
fun isOutUnread() = outRead < lastMessageId fun isOutUnread() = outRead - lastMessageId < 0
fun isUnread() = isInUnread() || isOutUnread() fun isUnread() = isInUnread() || isOutUnread()
fun isAccount() = id == UserConfig.userId
fun isPinned() = majorId > 0
} }
@@ -1,12 +1,12 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import androidx.lifecycle.MutableLiveData
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.model.SelectableItem import com.meloda.fast.model.SelectableItem
import com.meloda.fast.util.TimeUtils import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
@@ -14,7 +14,7 @@ import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages") @Entity(tableName = "messages")
@Parcelize @Parcelize
data class VkMessage( data class VkMessage constructor(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
var id: Int, var id: Int,
var text: String? = null, var text: String? = null,
@@ -28,21 +28,29 @@ data class VkMessage(
val actionText: String? = null, val actionText: String? = null,
val actionConversationMessageId: Int? = null, val actionConversationMessageId: Int? = null,
val actionMessage: String? = null, val actionMessage: String? = null,
val geoType: String? = null,
var updateTime: Int? = null,
var important: Boolean = false, var important: Boolean = false,
var forwards: List<VkMessage>? = null, var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null, var attachments: List<VkAttachment>? = null,
var replyMessage: VkMessage? = null var replyMessage: VkMessage? = null,
val geo: BaseVkMessage.Geo? = null,
) : SelectableItem(id) { ) : SelectableItem(id) {
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
val user = MutableLiveData<VkUser?>() var user: VkUser? = null
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
val group = MutableLiveData<VkGroup?>() var group: VkGroup? = null
@Ignore
@IgnoredOnParcel
var state: State = State.Sent
fun isPeerChat() = peerId > 2_000_000_000 fun isPeerChat() = peerId > 2_000_000_000
@@ -51,8 +59,11 @@ data class VkMessage(
fun isGroup() = fromId < 0 fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation) = fun isRead(conversation: VkConversation) =
if (isOut) conversation.outRead - id >= 0 if (isOut) {
else conversation.inRead - id >= 0 conversation.outRead - id >= 0
} else {
conversation.inRead - id >= 0
}
fun getPreparedAction(): Action? { fun getPreparedAction(): Action? {
if (action == null) return null if (action == null) return null
@@ -61,10 +72,27 @@ data class VkMessage(
fun canEdit() = fun canEdit() =
fromId == UserConfig.userId && fromId == UserConfig.userId &&
(attachments == null || !VKConstants.restrictedToEditAttachments.contains( (attachments == null ||
attachments!![0].javaClass !VKConstants.restrictedToEditAttachments.contains(
requireNotNull(attachments).first().javaClass
)) && )) &&
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS) (System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.OneDayInSeconds)
fun hasAttachments(): Boolean = !attachments.isNullOrEmpty()
fun hasReply(): Boolean = replyMessage != null
fun hasForwards(): Boolean = !forwards.isNullOrEmpty()
fun hasGeo(): Boolean = geo != null
fun isUpdated(): Boolean = updateTime != null && requireNotNull(updateTime) > 0
fun isSending(): Boolean = state == State.Sending
fun isError(): Boolean = state == State.Error
fun isSent(): Boolean = state == State.Sent
enum class Action(val value: String) { enum class Action(val value: String) {
CHAT_CREATE("chat_create"), CHAT_CREATE("chat_create"),
@@ -78,14 +106,17 @@ data class VkMessage(
CHAT_KICK_USER("chat_kick_user"), CHAT_KICK_USER("chat_kick_user"),
CHAT_SCREENSHOT("chat_screenshot"), CHAT_SCREENSHOT("chat_screenshot"),
// TODO: 9/11/2021 catch this shit
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"), CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"), CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
CHAT_STYLE_UPDATE("conversation_style_update"); CHAT_STYLE_UPDATE("conversation_style_update");
companion object { companion object {
fun parse(value: String) = values().first { it.value == value } fun parse(value: String?): Action? = values().firstOrNull { it.value == value }
} }
} }
enum class State {
Sending, Sent, Error
}
} }
@@ -1,10 +1,15 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import android.os.Parcelable import android.os.Parcelable
import com.meloda.fast.model.DataItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
open class VkAttachment : Parcelable { open class VkAttachment : DataItem<Int>(), Parcelable {
@IgnoredOnParcel
override val dataItemId: Int = -1
open fun asString(withAccessKey: Boolean = true) = "" open fun asString(withAccessKey: Boolean = true) = ""
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.VkUtils import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.base.attachments.BaseVkFile
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -12,7 +13,8 @@ data class VkFile(
val ext: String, val ext: String,
val size: Int, val size: Int,
val url: String, val url: String,
val accessKey: String? val accessKey: String?,
val preview: BaseVkFile.Preview?
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -12,7 +12,8 @@ data class VkVideo(
val ownerId: Int, val ownerId: Int,
val images: List<VideoImage>, val images: List<VideoImage>,
val firstFrames: List<BaseVkVideo.FirstFrame>?, val firstFrames: List<BaseVkVideo.FirstFrame>?,
val accessKey: String? val accessKey: String?,
val title: String
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -12,12 +12,12 @@ data class VkWall(
val date: Int, val date: Int,
val text: String, val text: String,
val attachments: List<BaseVkAttachmentItem>?, val attachments: List<BaseVkAttachmentItem>?,
val comments: Int, val comments: Int?,
val likes: Int, val likes: Int?,
val reposts: Int, val reposts: Int?,
val views: Int, val views: Int?,
val isFavorite: Boolean, val isFavorite: Boolean,
val accessKey: String val accessKey: String?
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -37,10 +37,11 @@ data class BaseVkConversation(
outRead = out_read, outRead = out_read,
isMarkedUnread = is_marked_unread, isMarkedUnread = is_marked_unread,
lastMessageId = last_message_id, lastMessageId = last_message_id,
unreadCount = unread_count, unreadCount = unread_count ?: 0,
membersCount = chat_settings?.members_count, membersCount = chat_settings?.members_count,
ownerId = chat_settings?.owner_id, ownerId = chat_settings?.owner_id,
isPinned = sort_id.major_id > 0, majorId = sort_id.major_id,
minorId = sort_id.minor_id,
canChangePin = chat_settings?.acl?.can_change_pin == true canChangePin = chat_settings?.acl?.can_change_pin == true
).apply { ).apply {
this.lastMessage = lastMessage this.lastMessage = lastMessage
@@ -24,7 +24,8 @@ data class BaseVkMessage(
val geo: Geo?, val geo: Geo?,
val action: Action?, val action: Action?,
val ttl: Int, val ttl: Int,
val reply_message: BaseVkMessage? val reply_message: BaseVkMessage?,
val update_time: Int?
) : Parcelable { ) : Parcelable {
fun asVkMessage() = VkMessage( fun asVkMessage() = VkMessage(
@@ -40,8 +41,9 @@ data class BaseVkMessage(
actionText = action?.text, actionText = action?.text,
actionConversationMessageId = action?.conversation_message_id, actionConversationMessageId = action?.conversation_message_id,
actionMessage = action?.message, actionMessage = action?.message,
geoType = geo?.type, geo = geo,
important = important important = important,
updateTime = update_time
).also { ).also {
it.attachments = VkUtils.parseAttachments(attachments) it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwd_messages) it.forwards = VkUtils.parseForwards(fwd_messages)
@@ -55,7 +57,6 @@ data class BaseVkMessage(
val place: Place val place: Place
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable
@@ -27,7 +27,8 @@ data class BaseVkFile(
ext = ext, ext = ext,
url = url, url = url,
size = size, size = size,
accessKey = access_key accessKey = access_key,
preview = preview
) )
@Parcelize @Parcelize
@@ -43,7 +43,8 @@ data class BaseVkVideo(
ownerId = owner_id, ownerId = owner_id,
images = image.map { it.asVideoImage() }, images = image.map { it.asVideoImage() },
firstFrames = first_frame, firstFrames = first_frame,
accessKey = access_key accessKey = access_key,
title = title
) )
@Parcelize @Parcelize
@@ -12,14 +12,14 @@ data class BaseVkWall(
val date: Int, val date: Int,
val text: String, val text: String,
val attachments: List<BaseVkAttachmentItem>?, val attachments: List<BaseVkAttachmentItem>?,
val post_source: PostSource, val post_source: PostSource?,
val comments: Comments, val comments: Comments?,
val likes: Likes, val likes: Likes?,
val reposts: Reposts, val reposts: Reposts?,
val views: Views, val views: Views?,
val is_favorite: Boolean, val is_favorite: Boolean,
val donut: Donut, val donut: Donut?,
val access_key: String, val access_key: String?,
val short_text_rate: Double val short_text_rate: Double
) : Parcelable { ) : Parcelable {
@@ -30,10 +30,10 @@ data class BaseVkWall(
date = date, date = date,
text = text, text = text,
attachments = attachments, attachments = attachments,
comments = comments.count, comments = comments?.count,
likes = likes.count, likes = likes?.count,
reposts = reposts.count, reposts = reposts?.count,
views = views.count, views = views?.count,
isFavorite = is_favorite, isFavorite = is_favorite,
accessKey = access_key accessKey = access_key
) )
@@ -0,0 +1,78 @@
package com.meloda.fast.api.network
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.base.ApiError
@Suppress("unused")
object VkErrorCodes {
const val UnknownError = 1
const val AppDisabled = 2
const val UnknownMethod = 3
const val InvalidSignature = 4
const val UserAuthorizationFailed = 5
const val TooManyRequests = 6
const val NoRights = 7
const val BadRequest = 8
const val TooManySimilarActions = 9
const val InternalServerError = 10
const val InTestMode = 11
const val ExecuteCodeCompileError = 12
const val ExecuteCodeRuntimeError = 13
const val CaptchaNeeded = 14
const val AccessDenied = 15
const val RequiresRequestsOverHttps = 16
const val ValidationRequired = 17
const val UserBannedOrDeleted = 18
const val ActionProhibited = 20
const val ActionAllowedOnlyForStandalone = 21
const val MethodOff = 23
const val ConfirmationRequired = 24
const val ParameterIsNotSpecified = 100
const val IncorrectAppId = 101
const val OutOfLimits = 103
const val IncorrectUserId = 113
const val IncorrectTimestamp = 150
const val AccessToAlbumDenied = 200
const val AccessToAudioDenied = 201
const val AccessToGroupDenied = 203
const val AlbumIsFull = 300
const val ActionDenied = 500
const val PermissionDenied = 600
const val CannotSendMessageBlackList = 900
const val CannotSendMessageGroup = 901
const val InvalidDocId = 1150
const val InvalidDocTitle = 1152
const val AccessToDocDenied = 1153
}
@Suppress("unused")
object VkErrors {
const val Unknown = "unknown_error"
const val NeedValidation = "need_validation"
const val NeedCaptcha = "need_captcha"
const val InvalidRequest = "invalid_request"
}
class AuthorizationError : ApiError()
data class ValidationRequiredError(
@SerializedName("validation_type")
val validationType: String,
@SerializedName("validation_sid")
val validationSid: String,
@SerializedName("phone_mask")
val phoneMask: String,
@SerializedName("redirect_uri")
val redirectUri: String,
@SerializedName("validation_resend")
val validationResend: String
) : ApiError()
data class CaptchaRequiredError(
@SerializedName("captcha_sid")
val captchaSid: String,
@SerializedName("captcha_img")
val captchaImg: String
) : ApiError()
@@ -11,16 +11,20 @@ class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().url.newBuilder() val builder = chain.request().url.newBuilder()
.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
val url = builder.build().toUrl().toString()
if (!builder.build().toUrl().toString().contains(AccountUrls.SetOnline)) if (!url.contains("upload.php")) {
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
}
if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) {
UserConfig.accessToken.let { UserConfig.accessToken.let {
if (it.isNotBlank()) if (it.isNotBlank())
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
} }
}
// TODO: 9/29/2021 crash on timeout
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
} }
@@ -1,51 +0,0 @@
package com.meloda.fast.api.network
object VkErrorCodes {
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
}
object VkErrors {
const val UNKNOWN = "unknown_error"
const val NEED_VALIDATION = "need_validation"
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_REQUEST = "invalid_request"
}
@@ -1,15 +1,18 @@
@file:Suppress("UNCHECKED_CAST")
package com.meloda.fast.api.network package com.meloda.fast.api.network
import com.meloda.fast.api.VKException import com.google.gson.Gson
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.base.ApiError import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.base.ApiResponse
import okhttp3.Request import okhttp3.Request
import okio.IOException
import okio.Timeout import okio.Timeout
import org.json.JSONObject
import retrofit2.* import retrofit2.*
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
class ResultCallFactory : CallAdapter.Factory() { class ResultCallFactory : CallAdapter.Factory() {
override fun get( override fun get(
@@ -21,7 +24,7 @@ class ResultCallFactory : CallAdapter.Factory() {
if (rawReturnType == Call::class.java) { if (rawReturnType == Call::class.java) {
if (returnType is ParameterizedType) { if (returnType is ParameterizedType) {
val callInnerType: Type = getParameterUpperBound(0, returnType) val callInnerType: Type = getParameterUpperBound(0, returnType)
if (getRawType(callInnerType) == Answer::class.java) { if (getRawType(callInnerType) == ApiAnswer::class.java) {
if (callInnerType is ParameterizedType) { if (callInnerType is ParameterizedType) {
val resultInnerType = getParameterUpperBound(0, callInnerType) val resultInnerType = getParameterUpperBound(0, callInnerType)
return ResultCallAdapter<Any?>(resultInnerType) return ResultCallAdapter<Any?>(resultInnerType)
@@ -55,16 +58,16 @@ internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : C
abstract fun cloneImpl(): Call<Out> abstract fun cloneImpl(): Call<Out>
} }
private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<Answer<R>>> { private class ResultCallAdapter<R>(private val type: Type) : CallAdapter<R, Call<ApiAnswer<R>>> {
override fun responseType() = type override fun responseType() = type
override fun adapt(call: Call<R>): Call<Answer<R>> = ResultCall(call) override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call)
} }
internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy) { internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, ApiAnswer<T>>(proxy) {
override fun enqueueImpl(callback: Callback<Answer<T>>) { override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
proxy.enqueue(ResultCallback(this, callback)) proxy.enqueue(ResultCallback(this, callback))
} }
@@ -74,25 +77,34 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
private class ResultCallback<T>( private class ResultCallback<T>(
private val proxy: ResultCall<T>, private val proxy: ResultCall<T>,
private val callback: Callback<Answer<T>> private val callback: Callback<ApiAnswer<T>>
) : Callback<T> { ) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) { val gson = Gson()
var isVkException = true
val result: Answer<T> = override fun onResponse(call: Call<T>, response: Response<T>) {
val result: ApiAnswer<T> =
if (response.isSuccessful) { if (response.isSuccessful) {
val baseBody = response.body() val baseBody = response.body()
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T) if (baseBody !is ApiResponse<*>) {
else { ApiAnswer.Success(baseBody as T)
val body = baseBody as ApiResponse<*> } else {
if (body.error != null) { val body = baseBody as? ApiResponse<*>
Answer.Error(body.error) if (body?.error != null) {
} else Answer.Success(body as T) VkUtils.getApiError(gson, gson.toJson(body.error))
} else {
ApiAnswer.Success(body as T)
} }
} else Answer.Error(IOException(response.errorBody()?.string() ?: "")) }
} else {
val errorBodyString = response.errorBody()?.string()
if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return VkUtils.getApiError(gson, errorBodyString)
}
if (checkErrors(call, result)) {
return
}
callback.onResponse(proxy, Response.success(result)) callback.onResponse(proxy, Response.success(result))
} }
@@ -100,30 +112,21 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
override fun onFailure(call: Call<T>, error: Throwable) { override fun onFailure(call: Call<T>, error: Throwable) {
callback.onResponse( callback.onResponse(
proxy, proxy,
Response.success(Answer.Error(throwable = error)) Response.success(ApiAnswer.Error(ApiError(throwable = error)))
) )
} }
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean { private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
if (result.throwable is ApiError) { if (!result.isSuccessful()) {
onFailure(call, result.throwable) result.error.throwable?.run {
onFailure(call, this)
return true return true
} }
} else {
return false
}
val json = JSONObject(result.throwable.message ?: "{}") return false
return if (json.has("error")) {
val error = json.optString("error", "")
val description = json.optString("error_description", "")
val exception = VKException(
error = error,
description = description,
).also { it.json = json }
onFailure(call, exception)
true
} else false
} }
} }
@@ -132,9 +135,16 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
} }
} }
sealed class Answer<out R> { sealed class ApiAnswer<out R> {
data class Success<out T>(val data: T) : Answer<T>() data class Success<out T>(val data: T) : ApiAnswer<T>()
data class Error(val throwable: Throwable) : Answer<Nothing>() data class Error(val error: ApiError) : ApiAnswer<Nothing>()
@OptIn(ExperimentalContracts::class)
fun isSuccessful(): Boolean {
contract {
returns(false) implies (this@ApiAnswer is Error)
}
return this is Success
}
} }
@@ -1,15 +0,0 @@
package com.meloda.fast.api.network.account
import javax.inject.Inject
class AccountDataSource @Inject constructor(
private val repo: AccountRepo
) {
suspend fun setOnline(params: AccountSetOnlineRequest) = repo.setOnline(params.map)
suspend fun setOffline(params: AccountSetOfflineRequest) = repo.setOffline(params.map)
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.audio
@@ -0,0 +1,20 @@
package com.meloda.fast.api.network.audio
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class AudiosGetUploadServerResponse(
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class AudiosUploadResponse(
val redirect: String,
val server: Int,
val audio: String?,
val hash: String,
val error: String?
) : Parcelable
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.audio
import com.meloda.fast.api.network.VkUrls
object AudiosUrls {
const val GetUploadServer = "${VkUrls.API}/audio.getUploadServer"
const val Save = "${VkUrls.API}/audio.save"
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.network.auth
import javax.inject.Inject
class AuthDataSource @Inject constructor(
private val repo: AuthRepo
) {
suspend fun auth(params: AuthDirectRequest) = repo.auth(params.map)
suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
}
@@ -1,16 +0,0 @@
package com.meloda.fast.api.network.auth
import com.meloda.fast.api.network.Answer
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthRepo {
@GET(AuthUrls.DirectAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<AuthDirectResponse>
@GET(AuthUrls.SendSms)
suspend fun sendSms(@Query("sid") validationSid: String): Answer<SendSmsResponse>
}
@@ -1,22 +0,0 @@
package com.meloda.fast.api.network.conversations
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.database.dao.ConversationsDao
import javax.inject.Inject
class ConversationsDataSource @Inject constructor(
private val repo: ConversationsRepo,
private val dao: ConversationsDao
) {
suspend fun get(params: ConversationsGetRequest) = repo.get(params.map)
suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map)
suspend fun pin(params: ConversationsPinRequest) = repo.pin(params.map)
suspend fun unpin(params: ConversationsUnpinRequest) = repo.unpin(params.map)
suspend fun store(conversations: List<VkConversation>) = dao.insert(conversations)
}
@@ -1,31 +0,0 @@
package com.meloda.fast.api.network.conversations
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConversationsRepo {
@FormUrlEncoded
@POST(ConversationsUrls.Get)
suspend fun get(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
@FormUrlEncoded
@POST(ConversationsUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.ReorderPinned)
suspend fun reorderPinned(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.files
@@ -0,0 +1,25 @@
package com.meloda.fast.api.network.files
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.base.attachments.BaseVkFile
import com.meloda.fast.api.model.base.attachments.BaseVkVoiceMessage
import kotlinx.parcelize.Parcelize
@Parcelize
data class FilesGetMessagesUploadServerResponse(
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class FilesUploadFileResponse(val file: String?, val error: String?) : Parcelable
@Parcelize
data class FilesSaveFileResponse(
val type: String,
@SerializedName("doc")
val file: BaseVkFile?,
@SerializedName("audio_message")
val voiceMessage: BaseVkVoiceMessage?
) : Parcelable
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.files
import com.meloda.fast.api.network.VkUrls
object FilesUrls {
const val GetMessagesUploadServer = "${VkUrls.API}/docs.getMessagesUploadServer"
const val Save = "${VkUrls.API}/docs.save"
}
@@ -9,7 +9,8 @@ data class LongPollGetUpdatesRequest(
val key: String, val key: String,
val ts: Int, val ts: Int,
val wait: Int, val wait: Int,
val mode: Int val mode: Int,
val version: Int
) : Parcelable { ) : Parcelable {
val map val map
@@ -18,7 +19,8 @@ data class LongPollGetUpdatesRequest(
"key" to key, "key" to key,
"ts" to ts.toString(), "ts" to ts.toString(),
"wait" to wait.toString(), "wait" to wait.toString(),
"mode" to mode.toString() "mode" to mode.toString(),
"version" to version.toString()
) )
} }
@@ -1,50 +0,0 @@
package com.meloda.fast.api.network.messages
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
import com.meloda.fast.api.network.longpoll.LongPollRepo
import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject
class MessagesDataSource @Inject constructor(
private val messagesRepo: MessagesRepo,
private val messagesDao: MessagesDao,
private val longPollRepo: LongPollRepo
) {
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
suspend fun getHistory(params: MessagesGetHistoryRequest) =
messagesRepo.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) =
messagesRepo.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
messagesRepo.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
messagesRepo.getLongPollServer(params.map)
suspend fun pin(params: MessagesPinMessageRequest) =
messagesRepo.pin(params.map)
suspend fun unpin(params: MessagesUnPinMessageRequest) =
messagesRepo.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) =
messagesRepo.delete(params.map)
suspend fun edit(params: MessagesEditRequest) =
messagesRepo.edit(params.map)
suspend fun getLongPollUpdates(
serverUrl: String,
params: LongPollGetUpdatesRequest
) = longPollRepo.getResponse(serverUrl, params.map)
suspend fun getById(params: MessagesGetByIdRequest) =
messagesRepo.getById(params.map)
}
@@ -41,7 +41,8 @@ data class MessagesSendRequest(
val stickerId: Int? = null, val stickerId: Int? = null,
val disableMentions: Boolean? = null, val disableMentions: Boolean? = null,
val dontParseLinks: Boolean? = null, val dontParseLinks: Boolean? = null,
val silent: Boolean? = null val silent: Boolean? = null,
val attachments: List<VkAttachment>? = null
) : Parcelable { ) : Parcelable {
val map val map
@@ -57,6 +58,11 @@ data class MessagesSendRequest(
disableMentions?.let { this["disable_mentions"] = it.intString } disableMentions?.let { this["disable_mentions"] = it.intString }
dontParseLinks?.let { this["dont_parse_links"] = it.intString } dontParseLinks?.let { this["dont_parse_links"] = it.intString }
silent?.let { this["silent"] = it.toString() } silent?.let { this["silent"] = it.toString() }
attachments?.let {
this["attachment"] = it.joinToString(separator = ",") { attachment ->
attachment.asString(true)
}
}
} }
} }
@@ -14,5 +14,6 @@ object MessagesUrls {
const val Delete = "${VkUrls.API}/messages.delete" const val Delete = "${VkUrls.API}/messages.delete"
const val Edit = "${VkUrls.API}/messages.edit" const val Edit = "${VkUrls.API}/messages.edit"
const val GetById = "${VkUrls.API}/messages.getById" const val GetById = "${VkUrls.API}/messages.getById"
const val MarkAsRead = "${VkUrls.API}/messages.markAsRead"
} }
@@ -0,0 +1,8 @@
package com.meloda.fast.api.network.ota
import android.os.Parcelable
import com.meloda.fast.model.UpdateItem
import kotlinx.parcelize.Parcelize
@Parcelize
data class OtaGetLatestReleaseResponse(val release: UpdateItem?) : Parcelable
@@ -0,0 +1,7 @@
package com.meloda.fast.api.network.ota
object OtaUrls {
const val GetActualUrl =
"https://raw.githubusercontent.com/melod1n/ota-server/master/ngrok_url.json"
}
@@ -0,0 +1,11 @@
package com.meloda.fast.api.network.photos
import com.meloda.fast.api.network.VkUrls
object PhotoUrls {
const val GetMessagesUploadServer = "${VkUrls.API}/photos.getMessagesUploadServer"
const val SaveMessagePhoto = "${VkUrls.API}/photos.saveMessagesPhoto"
}
@@ -0,0 +1,16 @@
package com.meloda.fast.api.network.photos
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PhotosSaveMessagePhotoRequest(
val photo: String, val server: Int, val hash: String
) : Parcelable {
val map
get() = mapOf(
"photo" to photo,
"server" to server.toString(),
"hash" to hash
)
}
@@ -0,0 +1,18 @@
package com.meloda.fast.api.network.photos
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class PhotosGetMessagesUploadServerResponse(
@SerializedName("album_id")
val albumId: Int,
@SerializedName("upload_url")
val uploadUrl: String
) : Parcelable
@Parcelize
data class PhotosUploadPhotoResponse(
val server: Int, val photo: String, val hash: String
) : Parcelable
@@ -1,16 +0,0 @@
package com.meloda.fast.api.network.users
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.database.dao.UsersDao
import javax.inject.Inject
class UsersDataSource @Inject constructor(
private val repo: UsersRepo,
private val dao: UsersDao
) {
suspend fun getById(params: UsersGetRequest) = repo.getById(params.map)
suspend fun storeUsers(users: List<VkUser>) = dao.insert(users)
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api.network.videos
@@ -0,0 +1,35 @@
package com.meloda.fast.api.network.videos
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class VideosSaveResponse(
@SerializedName("access_key")
val accessKey: String,
val description: String,
@SerializedName("owner_id")
val ownerId: Int,
val title: String,
@SerializedName("upload_url")
val uploadUrl: String,
@SerializedName("video_id")
val videoId: Int
) : Parcelable {
}
@Parcelize
data class VideosUploadResponse(
@SerializedName("video_hash")
val hash: String?,
val size: Int,
@SerializedName("direct_link")
val directLink: String,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("video_id")
val videoId: Int,
val error: String?
) : Parcelable
@@ -0,0 +1,9 @@
package com.meloda.fast.api.network.videos
import com.meloda.fast.api.network.VkUrls
object VideosUrls {
const val Save = "${VkUrls.API}/video.save"
}
@@ -1,7 +1,10 @@
package com.meloda.fast.base package com.meloda.fast.base
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.meloda.fast.screens.main.MainActivity
abstract class BaseFragment : Fragment { abstract class BaseFragment : Fragment {
@@ -9,4 +12,36 @@ abstract class BaseFragment : Fragment {
constructor(@LayoutRes resId: Int) : super(resId) constructor(@LayoutRes resId: Int) : super(resId)
protected var shouldNavBarShown: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments == null) arguments = Bundle()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity() as? MainActivity)?.run {
toggleNavBarVisibility(shouldNavBarShown)
}
}
val activityRouter
get() = run {
if (requireActivity() is MainActivity) {
(requireActivity() as MainActivity).router
} else {
null
}
}
fun requireActivityRouter() = requireNotNull(activityRouter)
fun startProgress() = toggleProgress(true)
fun stopProgress() = toggleProgress(false)
protected open fun toggleProgress(isProgressing: Boolean) {}
} }
@@ -1,45 +0,0 @@
package com.meloda.fast.base
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.IllegalTokenEvent
import com.meloda.fast.base.viewmodel.VkEvent
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
constructor() : super()
constructor(@LayoutRes resId: Int) : super(resId)
protected abstract val viewModel: VM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksEvent.onEach { onEvent(it) }.collect()
}
}
protected open fun onEvent(event: VkEvent) {
if (event is IllegalTokenEvent) {
Toast.makeText(
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
).show()
UserConfig.clear()
requireActivity().finishAffinity()
requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java))
}
}
}
@@ -1,12 +1,14 @@
package com.meloda.fast.base package com.meloda.fast.base
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
abstract class ResourceManager(protected val context: Context) { abstract class ResourceProvider(protected val context: Context) {
protected fun getString(@StringRes resId: Int): String { protected fun getString(@StringRes resId: Int): String {
return context.getString(resId) return context.getString(resId)
@@ -17,4 +19,8 @@ abstract class ResourceManager(protected val context: Context) {
return ContextCompat.getColor(context, resId) return ContextCompat.getColor(context, resId)
} }
protected fun getDrawable(@DrawableRes resId: Int): Drawable? {
return ContextCompat.getDrawable(context, resId)
}
} }
@@ -1,25 +1,26 @@
package com.meloda.fast.base.adapter package com.meloda.fast.base.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import com.meloda.fast.model.DataItem import com.meloda.fast.model.DataItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlin.properties.Delegates
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST") @Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
@SuppressLint("NotifyDataSetChanged")
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor( abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
var context: Context, var context: Context,
diffUtil: DiffUtil.ItemCallback<T>, diffUtil: DiffUtil.ItemCallback<T>,
preAddedValues: List<T> = emptyList(), preAddedValues: List<T> = emptyList(),
) : ListAdapter<T, VH>(diffUtil) { ) : ListAdapter<T, VH>(diffUtil), Filterable {
private var valuesFilter: ValuesFilter? = null
protected val adapterScope = CoroutineScope(Dispatchers.Default) protected val adapterScope = CoroutineScope(Dispatchers.Default)
private val cleanList = mutableListOf<T>() private val cleanList = mutableListOf<T>()
@@ -29,13 +30,19 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
var itemClickListener: ((position: Int) -> Unit)? = null var itemClickListener: ((position: Int) -> Unit)? = null
var itemLongClickListener: ((position: Int) -> Boolean)? = null var itemLongClickListener: ((position: Int) -> Boolean)? = null
private val listForSave = mutableListOf<T>()
var isSearching: Boolean by Delegates.observable(false) { _, _, _ ->
updateSearchingState()
}
init { init {
cleanList.addAll(preAddedValues) cleanList.addAll(preAddedValues)
addAll(preAddedValues) addAll(preAddedValues)
} }
fun cloneCurrentList(): MutableList<T> { fun cloneCurrentList(): MutableList<T> {
return ArrayList(currentList) return currentList.toMutableList()
} }
open fun destroy() {} open fun destroy() {}
@@ -142,6 +149,11 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
return currentList.indexOf(item) return currentList.indexOf(item)
} }
fun searchIndexOf(item: T): Int? {
val index = indexOf(item)
return if (index == -1) null else index
}
val indices get() = currentList.indices val indices get() = currentList.indices
operator fun get(position: Int): T { operator fun get(position: Int): T {
@@ -161,9 +173,8 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
fun isEmpty() = currentList.isEmpty() fun isEmpty() = currentList.isEmpty()
fun isNotEmpty() = currentList.isNotEmpty() fun isNotEmpty() = currentList.isNotEmpty()
@SuppressLint("NotifyDataSetChanged")
fun refreshList() { fun refreshList() {
notifyDataSetChanged() notifyItemRangeChanged(0, itemCount)
} }
fun updateCleanList(list: List<T>?) { fun updateCleanList(list: List<T>?) {
@@ -201,4 +212,86 @@ abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
} }
val lastPosition get() = currentList.lastIndex val lastPosition get() = currentList.lastIndex
private fun updateSearchingState() {
Log.d("BaseAdapter", "updateSearchingState: $isSearching")
cleanList.clear()
if (isSearching) {
listForSave.clear()
listForSave += cloneCurrentList()
} else {
setItems(listForSave, commitCallback = {
listForSave.clear()
})
}
}
open fun filter(query: String) {
if (cleanList.isEmpty()) {
cleanList.addAll(listForSave)
}
val newList = mutableListOf<T>()
setItems(emptyList(), commitCallback = {
if (query.isEmpty()) {
newList.addAll(cleanList)
} else {
for (item in cleanList) {
if (onQueryItem(item, query)) {
newList.add(item)
}
}
}
setItems(newList)
})
}
open fun onQueryItem(item: T, query: String): Boolean {
return false
}
override fun getFilter(): Filter {
if (valuesFilter == null) {
valuesFilter = ValuesFilter()
}
return requireNotNull(valuesFilter)
}
private inner class ValuesFilter : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val results = FilterResults()
if (isEmpty()) return results
if (!constraint.isNullOrEmpty()) {
val filteredList = mutableListOf<T>()
for (item in listForSave) {
if (onQueryItem(item, constraint.toString())) {
filteredList.add(item)
}
}
results.count = filteredList.size
results.values = filteredList
} else {
results.count = listForSave.size
results.values = listForSave
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
val items = results.values as? List<T>
setItems(items)
}
}
override fun onCurrentListChanged(previousList: MutableList<T>, currentList: MutableList<T>) {
super.onCurrentListChanged(previousList, currentList)
}
} }
@@ -1,3 +0,0 @@
package com.meloda.fast.base.adapter
abstract class BaseItem
@@ -2,7 +2,6 @@ package com.meloda.fast.base.adapter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
@@ -13,5 +12,3 @@ abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
open fun bind(position: Int, payloads: MutableList<Any>?) {} open fun bind(position: Int, payloads: MutableList<Any>?) {}
} }
abstract class BindingHolder<B : ViewBinding>(protected val binding: B) : BaseHolder(binding.root)
@@ -2,15 +2,19 @@ package com.meloda.fast.base.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.VKException
import com.meloda.fast.api.base.ApiError import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.VkErrorCodes import com.meloda.fast.api.network.AuthorizationError
import com.meloda.fast.api.network.VkErrors import com.meloda.fast.api.network.CaptchaRequiredError
import com.meloda.fast.api.network.ValidationRequiredError
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseViewModel : ViewModel() { abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = "" var unknownErrorDefaultText: String = ""
@@ -18,19 +22,47 @@ abstract class BaseViewModel : ViewModel() {
protected val tasksEventChannel = Channel<VkEvent>() protected val tasksEventChannel = Channel<VkEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow() val tasksEvent = tasksEventChannel.receiveAsFlow()
protected val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
viewModelScope.launch { onException(throwable) }
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(exceptionHandler, block = block)
}
protected suspend fun <T> makeSuspendJob(
job: suspend () -> ApiAnswer<T>, onAnswer: suspend (T) -> Unit = {},
onStart: (suspend () -> Unit)? = null,
onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null
): ApiAnswer<T> {
onStart?.invoke() ?: onStart()
val response = job()
when (response) {
is ApiAnswer.Success -> onAnswer(response.data)
is ApiAnswer.Error -> {
onError?.invoke(response.error) ?: checkErrors(response.error)
}
}
onEnd?.invoke()
return response
}
protected fun <T> makeJob( protected fun <T> makeJob(
job: suspend () -> Answer<T>, job: suspend () -> ApiAnswer<T>,
onAnswer: suspend (T) -> Unit = {}, onAnswer: suspend (T) -> Unit = {},
onStart: (suspend () -> Unit)? = null, onStart: (suspend () -> Unit)? = null,
onEnd: (suspend () -> Unit)? = null, onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null onError: (suspend (Throwable) -> Unit)? = null
) = viewModelScope.launch { ): Job = viewModelScope.launch {
onStart?.invoke() ?: onStart() onStart?.invoke() ?: onStart()
when (val response = job()) { when (val response = job()) {
is Answer.Success -> onAnswer(response.data) is ApiAnswer.Success -> onAnswer(response.data)
is Answer.Error -> { is ApiAnswer.Error -> {
checkErrors(response.throwable) onError?.invoke(response.error) ?: checkErrors(response.error)
onError?.invoke(response.throwable) ?: onError(response.throwable)
} }
} }
}.also { }.also {
@@ -41,6 +73,10 @@ abstract class BaseViewModel : ViewModel() {
} }
} }
protected open suspend fun onException(throwable: Throwable) {
checkErrors(throwable)
}
protected suspend fun onStart() { protected suspend fun onStart() {
sendEvent(StartProgressEvent) sendEvent(StartProgressEvent)
} }
@@ -49,37 +85,24 @@ abstract class BaseViewModel : ViewModel() {
sendEvent(StopProgressEvent) sendEvent(StopProgressEvent)
} }
protected suspend fun onError(throwable: Throwable) {
sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText))
}
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event) protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
private suspend fun checkErrors(throwable: Throwable) { protected suspend fun checkErrors(throwable: Throwable) {
when (throwable) { when (throwable) {
is AuthorizationError -> {
sendEvent(AuthorizationErrorEvent)
}
is ValidationRequiredError -> {
sendEvent(ValidationRequiredEvent(throwable.validationSid))
}
is CaptchaRequiredError -> {
sendEvent(CaptchaRequiredEvent(throwable.captchaSid, throwable.captchaImg))
}
is ApiError -> { is ApiError -> {
when (throwable.errorCode) { sendEvent(ErrorTextEvent(errorText = throwable.errorMessage ?: unknownErrorDefaultText))
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent)
}
}
}
is VKException -> {
when (throwable.error) {
VkErrors.NEED_CAPTCHA -> {
val json = throwable.json ?: return
sendEvent(
CaptchaEvent(
sid = json.optString("captcha_sid"),
image = json.optString("captcha_img")
)
)
}
VkErrors.NEED_VALIDATION -> {
val json = throwable.json ?: return
sendEvent(ValidationEvent(sid = json.optString("validation_sid")))
}
} }
else -> {
sendEvent(ErrorTextEvent(throwable.message ?: unknownErrorDefaultText))
} }
} }
} }
@@ -0,0 +1,34 @@
package com.meloda.fast.base.viewmodel
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.base.BaseFragment
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
constructor() : super()
constructor(@LayoutRes resId: Int) : super(resId)
protected abstract val viewModel: VM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToViewModel(viewModel)
}
protected open fun onEvent(event: VkEvent) {
ViewModelUtils.parseEvent(this, event)
}
protected fun <T : BaseViewModel> subscribeToViewModel(viewModel: T) {
lifecycleScope.launch {
viewModel.tasksEvent.collect { onEvent(it) }
}
}
}
@@ -1,22 +1,17 @@
package com.meloda.fast.base.viewmodel package com.meloda.fast.base.viewmodel
data class ShowDialogInfoEvent(
val title: String? = null,
val message: String,
val positiveBtn: String? = null,
val negativeBtn: String? = null
) : VkEvent()
data class ErrorEvent(val errorText: String) : VkEvent()
object IllegalTokenEvent : VkEvent()
data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
data class ValidationEvent(val sid: String) : VkEvent()
object StartProgressEvent : VkEvent()
object StopProgressEvent : VkEvent()
abstract class VkEvent abstract class VkEvent
abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent()
abstract class VkProgressEvent : VkEvent()
open class ErrorTextEvent(override val errorText: String) : VkErrorEvent()
object AuthorizationErrorEvent : VkErrorEvent()
data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent()
data class ValidationRequiredEvent(val sid: String) : VkErrorEvent()
object StartProgressEvent : VkProgressEvent()
object StopProgressEvent : VkProgressEvent()
interface VkEventCallback<in T : Any> { interface VkEventCallback<in T : Any> {
fun onEvent(event: T) fun onEvent(event: T)
@@ -0,0 +1,49 @@
package com.meloda.fast.base.viewmodel
import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.screens.main.MainActivity
import com.meloda.fast.util.ViewUtils.showErrorDialog
object ViewModelUtils {
@Suppress("MemberVisibilityCanBePrivate")
fun parseEvent(activity: FragmentActivity, event: VkEvent) {
when (event) {
is AuthorizationErrorEvent -> {
Toast.makeText(
activity, R.string.authorization_failed, Toast.LENGTH_LONG
).show()
UserConfig.clear()
activity.finishAffinity()
activity.startActivity(Intent(activity, MainActivity::class.java))
}
is VkErrorEvent -> {
event.errorText?.run {
activity.showErrorDialog(this)
}
}
}
}
fun parseEvent(fragment: Fragment, event: VkEvent) {
if (event is VkProgressEvent) {
if (fragment is BaseFragment) {
if (event is StartProgressEvent) {
fragment.startProgress()
} else if (event is StopProgressEvent) {
fragment.stopProgress()
}
}
} else {
parseEvent(fragment.requireActivity(), event)
}
}
}
@@ -0,0 +1,7 @@
package com.meloda.fast.common
object AppConstants {
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
}
@@ -1,6 +1,7 @@
package com.meloda.fast.common package com.meloda.fast.common
import android.app.Application import android.app.Application
import android.app.DownloadManager
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -12,10 +13,9 @@ import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import com.meloda.fast.BuildConfig
import com.meloda.fast.database.AppDatabase import com.meloda.fast.database.AppDatabase
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
@HiltAndroidApp @HiltAndroidApp
@@ -26,6 +26,7 @@ class AppGlobal : Application() {
lateinit var inputMethodManager: InputMethodManager lateinit var inputMethodManager: InputMethodManager
lateinit var connectivityManager: ConnectivityManager lateinit var connectivityManager: ConnectivityManager
lateinit var clipboardManager: ClipboardManager lateinit var clipboardManager: ClipboardManager
lateinit var downloadManager: DownloadManager
lateinit var preferences: SharedPreferences lateinit var preferences: SharedPreferences
lateinit var resources: Resources lateinit var resources: Resources
@@ -37,11 +38,13 @@ class AppGlobal : Application() {
lateinit var packageManager: PackageManager lateinit var packageManager: PackageManager
var versionName = "" var versionName = ""
var versionCode = 0L var versionCode = 0
var screenWidth = 0 var screenWidth = 0
var screenHeight = 0 var screenHeight = 0
var screenWidth80 = 0
val Instance get() = instance val Instance get() = instance
} }
@@ -49,19 +52,15 @@ class AppGlobal : Application() {
super.onCreate() super.onCreate()
instance = this instance = this
if (!BuildConfig.DEBUG) {
ACRA.init(this)
}
appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache") appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
.fallbackToDestructiveMigration() // .fallbackToDestructiveMigration()
.build() .build()
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES) val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
versionName = info.versionName versionName = info.versionName
versionCode = PackageInfoCompat.getLongVersionCode(info) versionCode = PackageInfoCompat.getLongVersionCode(info).toInt()
Companion.resources = resources Companion.resources = resources
Companion.packageName = packageName Companion.packageName = packageName
@@ -70,6 +69,8 @@ class AppGlobal : Application() {
screenWidth = resources.displayMetrics.widthPixels screenWidth = resources.displayMetrics.widthPixels
screenHeight = resources.displayMetrics.heightPixels screenHeight = resources.displayMetrics.heightPixels
screenWidth80 = (screenWidth * 0.8).roundToInt()
val density = resources.displayMetrics.density val density = resources.displayMetrics.density
val densityDpi = resources.displayMetrics.densityDpi val densityDpi = resources.displayMetrics.densityDpi
val densityScaled = resources.displayMetrics.scaledDensity val densityScaled = resources.displayMetrics.scaledDensity
@@ -82,11 +83,12 @@ class AppGlobal : Application() {
Log.i( Log.i(
"Fast::DeviceInfo", "Fast::DeviceInfo",
"width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi" "width: $screenWidth; 70% width: $screenWidth80; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
) )
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
} }
} }
@@ -12,7 +12,7 @@ import kotlinx.coroutines.Job
object AppSettings { object AppSettings {
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled") val keyUseNavigationDrawer = booleanPreferencesKey("use_nav_drawer")
} }
@@ -1,17 +1,50 @@
package com.meloda.fast.common package com.meloda.fast.common
import android.os.Bundle
import com.github.terrakok.cicerone.androidx.FragmentScreen import com.github.terrakok.cicerone.androidx.FragmentScreen
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.model.UpdateItem
import com.meloda.fast.screens.conversations.ConversationsFragment import com.meloda.fast.screens.conversations.ConversationsFragment
import com.meloda.fast.screens.login.LoginFragment import com.meloda.fast.screens.login.LoginFragment
import com.meloda.fast.screens.main.MainFragment import com.meloda.fast.screens.main.MainFragment
import com.meloda.fast.screens.messages.ForwardedMessagesFragment
import com.meloda.fast.screens.messages.MessagesHistoryFragment import com.meloda.fast.screens.messages.MessagesHistoryFragment
import com.meloda.fast.screens.settings.SettingsRootFragment
import com.meloda.fast.screens.updates.UpdatesFragment
@Suppress("FunctionName") @Suppress("FunctionName")
object Screens { object Screens {
fun Main() = FragmentScreen { MainFragment() } fun Main() = FragmentScreen { MainFragment() }
fun Login() = FragmentScreen { LoginFragment() }
fun Conversations() = FragmentScreen { ConversationsFragment() } fun Login(
fun MessagesHistory(bundle: Bundle) = getFastToken: Boolean = false
FragmentScreen { MessagesHistoryFragment.newInstance(bundle) } ) = FragmentScreen {
LoginFragment.newInstance(getFastToken)
}
fun Conversations() = FragmentScreen { ConversationsFragment() }
fun MessagesHistory(
conversation: VkConversation,
user: VkUser?,
group: VkGroup?
) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) }
fun ForwardedMessages(
conversation: VkConversation,
messages: List<VkMessage>,
profiles: HashMap<Int, VkUser> = hashMapOf(),
groups: HashMap<Int, VkGroup> = hashMapOf()
) = FragmentScreen {
ForwardedMessagesFragment.newInstance(
conversation, messages, profiles, groups
)
}
fun Updates(updateItem: UpdateItem? = null) =
FragmentScreen { UpdatesFragment.newInstance(updateItem) }
fun Settings() = FragmentScreen { SettingsRootFragment() }
} }
@@ -1,98 +0,0 @@
package com.meloda.fast.common
import android.content.Context
import android.content.IntentFilter
import com.meloda.fast.receiver.MinuteReceiver
import java.util.*
object TimeManager {
var currentHour = 0
var currentMinute = 0
var currentSecond = 0
private val onHourChangeListeners: ArrayList<OnHourChangeListener> = ArrayList()
private val onMinuteChangeListeners: ArrayList<OnMinuteChangeListener> = ArrayList()
private val onSecondChangeListeners: ArrayList<OnSecondChangeListener> = ArrayList()
private val onTimeChangeListeners: ArrayList<OnTimeChangeListener> = ArrayList()
fun init(context: Context) {
context.registerReceiver(MinuteReceiver(), IntentFilter("android.intent.action.TIME_TICK"))
addOnMinuteChangeListener(minuteChangeListener)
}
private var minuteChangeListener = object : OnMinuteChangeListener {
override fun onMinuteChange(currentMinute: Int) {
TimeManager.currentMinute = currentMinute
}
}
fun destroy() {
removeOnMinuteChangeListener(minuteChangeListener)
}
fun broadcastMinute() {
for (onMinuteChangeListener in onMinuteChangeListeners) {
onMinuteChangeListener.onMinuteChange(0)
}
}
val isMorning = currentHour in 7..11
val isAfternoon = currentHour in 12..16
val isEvening = currentHour in 17..22
val isNight = currentHour == 23 || currentHour < 6 && currentHour > -1
fun addOnHourChangeListener(onHourChangeListeners: OnHourChangeListener) {
TimeManager.onHourChangeListeners.add(onHourChangeListeners)
}
fun removeOnHourChangeListener(onHourChangeListener: OnHourChangeListener?) {
onHourChangeListeners.remove(onHourChangeListener)
}
fun addOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener) {
onMinuteChangeListeners.add(onMinuteChangeListener)
}
fun removeOnMinuteChangeListener(onMinuteChangeListener: OnMinuteChangeListener?) {
onMinuteChangeListeners.remove(onMinuteChangeListener)
}
fun addOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener) {
onSecondChangeListeners.add(onSecondChangeListener)
}
fun removeOnSecondChangeListener(onSecondChangeListener: OnSecondChangeListener?) {
onSecondChangeListeners.remove(onSecondChangeListener)
}
fun addOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener) {
onTimeChangeListeners.add(onTimeChangeListener)
}
fun removeOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
onTimeChangeListeners.remove(onTimeChangeListener)
}
interface OnHourChangeListener {
fun onHourChange(currentHour: Int)
}
interface OnMinuteChangeListener {
fun onMinuteChange(currentMinute: Int)
}
interface OnSecondChangeListener {
fun onSecondChange(currentSecond: Int)
}
interface OnTimeChangeListener {
fun onHourChange(currentHour: Int)
fun onMinuteChange(currentMinute: Int)
fun onSecondChange(currentSecond: Int)
}
}
@@ -0,0 +1,100 @@
package com.meloda.fast.common
import androidx.lifecycle.MutableLiveData
import com.meloda.fast.BuildConfig
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
import com.meloda.fast.data.ota.OtaApi
import com.meloda.fast.extensions.setIfNotEquals
import com.meloda.fast.model.UpdateActualUrl
import com.meloda.fast.model.UpdateItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import kotlin.coroutines.CoroutineContext
class UpdateManager(private val repo: OtaApi) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default
companion object {
val newUpdate = MutableLiveData<UpdateItem?>(null)
val updateError = MutableLiveData<Throwable?>(null)
var otaBaseUrl: String? = null
private set
}
private var listener: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null
private fun getActualUrl() = launch {
val job: suspend () -> ApiAnswer<UpdateActualUrl> = { repo.getActualUrl() }
when (val jobResponse = job()) {
is ApiAnswer.Success -> {
val item = jobResponse.data
otaBaseUrl = item.url
getLatestRelease()
}
is ApiAnswer.Error -> {
otaBaseUrl = null
val throwable = jobResponse.error.throwable
listener?.invoke(null, throwable)
withContext(Dispatchers.Main) {
updateError.setIfNotEquals(throwable)
}
}
}
}
private fun getLatestRelease() = launch {
val url = "$otaBaseUrl/releases-latest"
val job: suspend () -> ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>> = {
repo.getLatestRelease(url = url, secretCode = getOtaSecret())
}
withContext(Dispatchers.Main) {
when (val jobResponse = job()) {
is ApiAnswer.Success -> {
val response = jobResponse.data.response ?: return@withContext
val latestRelease = response.release
if (latestRelease != null &&
(AppGlobal.versionName
.split("_")
.getOrNull(1) != latestRelease.versionName ||
AppGlobal.versionCode < latestRelease.versionCode)
) {
newUpdate.setIfNotEquals(latestRelease)
listener?.invoke(latestRelease, null)
} else {
newUpdate.setIfNotEquals(null)
listener?.invoke(null, null)
}
}
is ApiAnswer.Error -> {
val throwable = jobResponse.error.throwable
updateError.setIfNotEquals(throwable)
listener?.invoke(null, throwable)
}
}
}
}
private fun getOtaSecret(): String {
return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8")
}
fun checkUpdates(block: ((item: UpdateItem?, error: Throwable?) -> Unit)? = null) = launch {
this@UpdateManager.listener = block
getActualUrl()
}
}
@@ -1,17 +1,18 @@
package com.meloda.fast.api.network.account package com.meloda.fast.data.account
import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.account.AccountUrls
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.QueryMap import retrofit2.http.QueryMap
interface AccountRepo { interface AccountApi {
@GET(AccountUrls.SetOnline) @GET(AccountUrls.SetOnline)
suspend fun setOnline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun setOnline(@QueryMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@POST(AccountUrls.SetOffline) @POST(AccountUrls.SetOffline)
suspend fun setOffline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun setOffline(@QueryMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
} }
@@ -0,0 +1,18 @@
package com.meloda.fast.data.account
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.meloda.fast.model.AppAccount
@Dao
interface AccountsDao {
@Query("SELECT * FROM accounts")
suspend fun getAll(): List<AppAccount>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(values: List<AppAccount>)
}
@@ -0,0 +1,17 @@
package com.meloda.fast.data.account
import com.meloda.fast.api.network.account.AccountSetOfflineRequest
import com.meloda.fast.api.network.account.AccountSetOnlineRequest
class AccountsRepository(
private val accountApi: AccountApi,
private val accountsDao: AccountsDao
) {
suspend fun setOnline(params: AccountSetOnlineRequest) = accountApi.setOnline(params.map)
suspend fun setOffline(params: AccountSetOfflineRequest) = accountApi.setOffline(params.map)
}
@@ -0,0 +1,28 @@
package com.meloda.fast.data.audios
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.attachments.BaseVkAudio
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.audio.AudiosGetUploadServerResponse
import com.meloda.fast.api.network.audio.AudiosUploadResponse
import com.meloda.fast.api.network.audio.AudiosUrls
import okhttp3.MultipartBody
import retrofit2.http.*
interface AudiosApi {
@POST(AudiosUrls.GetUploadServer)
suspend fun getUploadServer(): ApiAnswer<ApiResponse<AudiosGetUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<AudiosUploadResponse>
@FormUrlEncoded
@POST(AudiosUrls.Save)
suspend fun save(@FieldMap map: Map<String, String>): ApiAnswer<ApiResponse<BaseVkAudio>>
}
@@ -0,0 +1,21 @@
package com.meloda.fast.data.audios
import okhttp3.MultipartBody
class AudiosRepository(
private val audiosApi: AudiosApi
) {
suspend fun getUploadServer() = audiosApi.getUploadServer()
suspend fun upload(url: String, file: MultipartBody.Part) = audiosApi.upload(url, file)
suspend fun save(server: Int, audio: String, hash: String) = audiosApi.save(
mapOf(
"server" to server.toString(),
"audio" to audio,
"hash" to hash
)
)
}
@@ -0,0 +1,19 @@
package com.meloda.fast.data.auth
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.auth.AuthDirectResponse
import com.meloda.fast.api.network.auth.AuthUrls
import com.meloda.fast.api.network.auth.SendSmsResponse
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthApi {
@GET(AuthUrls.DirectAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): ApiAnswer<AuthDirectResponse>
@GET(AuthUrls.SendSms)
suspend fun sendSms(@Query("sid") validationSid: String): ApiAnswer<SendSmsResponse>
}
@@ -0,0 +1,14 @@
package com.meloda.fast.data.auth
import com.meloda.fast.api.network.auth.AuthDirectRequest
class AuthRepository(
private val authApi: AuthApi
) {
suspend fun auth(params: AuthDirectRequest) = authApi.auth(params.map)
suspend fun sendSms(validationSid: String) = authApi.sendSms(validationSid)
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.conversations
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.conversations.ConversationsGetResponse
import com.meloda.fast.api.network.conversations.ConversationsUrls
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConversationsApi {
@FormUrlEncoded
@POST(ConversationsUrls.Get)
suspend fun get(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<ConversationsGetResponse>>
@FormUrlEncoded
@POST(ConversationsUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded
@POST(ConversationsUrls.ReorderPinned)
suspend fun reorderPinned(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao package com.meloda.fast.data.conversations
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@@ -15,6 +15,4 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(values: List<VkConversation>) suspend fun insert(values: List<VkConversation>)
suspend fun insert(values: Array<out VkConversation>) = insert(values.toList())
} }
@@ -0,0 +1,25 @@
package com.meloda.fast.data.conversations
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest
import com.meloda.fast.api.network.conversations.ConversationsGetRequest
import com.meloda.fast.api.network.conversations.ConversationsPinRequest
import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest
import kotlinx.coroutines.sync.Mutex
class ConversationsRepository(
private val conversationsApi: ConversationsApi,
private val conversationsDao: ConversationsDao
) {
suspend fun get(params: ConversationsGetRequest) = conversationsApi.get(params.map)
suspend fun delete(params: ConversationsDeleteRequest) = conversationsApi.delete(params.map)
suspend fun pin(params: ConversationsPinRequest) = conversationsApi.pin(params.map)
suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map)
suspend fun store(conversations: List<VkConversation>) = conversationsDao.insert(conversations)
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.files
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.files.FilesGetMessagesUploadServerResponse
import com.meloda.fast.api.network.files.FilesSaveFileResponse
import com.meloda.fast.api.network.files.FilesUploadFileResponse
import com.meloda.fast.api.network.files.FilesUrls
import okhttp3.MultipartBody
import retrofit2.http.*
interface FilesApi {
@FormUrlEncoded
@POST(FilesUrls.GetMessagesUploadServer)
suspend fun getUploadServer(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<FilesGetMessagesUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<FilesUploadFileResponse>
@FormUrlEncoded
@POST(FilesUrls.Save)
suspend fun save(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<FilesSaveFileResponse>>
}
@@ -0,0 +1,30 @@
package com.meloda.fast.data.files
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
class FilesRepository(
private val filesApi: FilesApi
) {
enum class FileType(val value: String) {
@SerializedName("doc")
File("doc"),
@SerializedName("audio_message")
VoiceMessage("audio_message")
}
suspend fun getMessagesUploadServer(peerId: Int, type: FileType) =
filesApi.getUploadServer(
mapOf(
"peer_id" to peerId.toString(),
"type" to type.value
)
)
suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesApi.upload(url, file)
suspend fun saveMessageFile(file: String) = filesApi.save(mapOf("file" to file))
}
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao package com.meloda.fast.data.groups
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@@ -0,0 +1,6 @@
package com.meloda.fast.data.groups
class GroupsRepository(
private val groupsDao: GroupsDao
) {
}
@@ -1,17 +1,17 @@
package com.meloda.fast.api.network.longpoll package com.meloda.fast.data.longpoll
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.QueryMap import retrofit2.http.QueryMap
import retrofit2.http.Url import retrofit2.http.Url
interface LongPollRepo { interface LongPollApi {
@GET @GET
suspend fun getResponse( suspend fun getResponse(
@Url serverUrl: String, @Url serverUrl: String,
@QueryMap params: Map<String, String> @QueryMap params: Map<String, String>
): Answer<JsonObject> ): ApiAnswer<JsonObject>
} }
@@ -1,49 +1,56 @@
package com.meloda.fast.api.network.messages package com.meloda.fast.data.messages
import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkLongPoll import com.meloda.fast.api.model.base.BaseVkLongPoll
import com.meloda.fast.api.model.base.BaseVkMessage import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.messages.MessagesGetByIdResponse
import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse
import com.meloda.fast.api.network.messages.MessagesUrls
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
interface MessagesRepo { interface MessagesApi {
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.GetHistory) @POST(MessagesUrls.GetHistory)
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>> suspend fun getHistory(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<MessagesGetHistoryResponse>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.Send) @POST(MessagesUrls.Send)
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>> suspend fun send(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.MarkAsImportant) @POST(MessagesUrls.MarkAsImportant)
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>> suspend fun markAsImportant(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<List<Int>>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.GetLongPollServer) @POST(MessagesUrls.GetLongPollServer)
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>> suspend fun getLongPollServer(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<BaseVkLongPoll>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.Pin) @POST(MessagesUrls.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkMessage>> suspend fun pin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<BaseVkMessage>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.Unpin) @POST(MessagesUrls.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun unpin(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.Delete) @POST(MessagesUrls.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun delete(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.Edit) @POST(MessagesUrls.Edit)
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun edit(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Any>>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.GetById) @POST(MessagesUrls.GetById)
suspend fun getById(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetByIdResponse>> suspend fun getById(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<MessagesGetByIdResponse>>
@FormUrlEncoded
@POST(MessagesUrls.MarkAsRead)
suspend fun markAsRead(@FieldMap params: Map<String, String>): ApiAnswer<ApiResponse<Int>>
} }
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao package com.meloda.fast.data.messages
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@@ -0,0 +1,65 @@
package com.meloda.fast.data.messages
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.api.network.messages.*
class MessagesRepository(
private val messagesApi: MessagesApi,
private val messagesDao: MessagesDao,
private val longPollApi: LongPollApi
) {
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
suspend fun getHistory(params: MessagesGetHistoryRequest) =
messagesApi.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) =
messagesApi.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
messagesApi.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
messagesApi.getLongPollServer(params.map)
suspend fun pin(params: MessagesPinMessageRequest) =
messagesApi.pin(params.map)
suspend fun unpin(params: MessagesUnPinMessageRequest) =
messagesApi.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) =
messagesApi.delete(params.map)
suspend fun edit(params: MessagesEditRequest) =
messagesApi.edit(params.map)
suspend fun getLongPollUpdates(
serverUrl: String,
params: LongPollGetUpdatesRequest
) = longPollApi.getResponse(serverUrl, params.map)
suspend fun getById(params: MessagesGetByIdRequest) =
messagesApi.getById(params.map)
suspend fun markAsRead(
peerId: Int,
messagesIds: List<Int>? = null,
startMessageId: Int? = null
) = messagesApi.markAsRead(
mutableMapOf("peer_id" to peerId.toString()).apply {
messagesIds?.let {
this["message_ids"] = messagesIds.joinToString { it.toString() }
}
startMessageId?.let {
this["start_message_id"] = it.toString()
}
}
)
}
@@ -0,0 +1,26 @@
package com.meloda.fast.data.ota
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse
import com.meloda.fast.api.network.ota.OtaUrls
import com.meloda.fast.model.UpdateActualUrl
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
import retrofit2.http.Url
interface OtaApi {
@GET(OtaUrls.GetActualUrl)
suspend fun getActualUrl(): ApiAnswer<UpdateActualUrl>
@GET
suspend fun getLatestRelease(
@Url url: String,
@Query("productId") productId: Int = 28,
@Query("branchId") branchId: Int = 10,
@Header("Secret-Code") secretCode: String
): ApiAnswer<ApiResponse<OtaGetLatestReleaseResponse>>
}
@@ -0,0 +1,33 @@
package com.meloda.fast.data.photos
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.photos.PhotoUrls
import com.meloda.fast.api.network.photos.PhotosGetMessagesUploadServerResponse
import com.meloda.fast.api.network.photos.PhotosUploadPhotoResponse
import okhttp3.MultipartBody
import retrofit2.http.*
interface PhotosApi {
@FormUrlEncoded
@POST(PhotoUrls.GetMessagesUploadServer)
suspend fun getUploadServer(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<PhotosGetMessagesUploadServerResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part photo: MultipartBody.Part
): ApiAnswer<PhotosUploadPhotoResponse>
@FormUrlEncoded
@POST(PhotoUrls.SaveMessagePhoto)
suspend fun save(
@FieldMap map: Map<String, String>
): ApiAnswer<ApiResponse<List<BaseVkPhoto>>>
}
@@ -0,0 +1,18 @@
package com.meloda.fast.data.photos
import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest
import okhttp3.MultipartBody
class PhotosRepository(
private val photosApi: PhotosApi
) {
suspend fun getMessagesUploadServer(peerId: Int) =
photosApi.getUploadServer(mapOf("peer_id" to peerId.toString()))
suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = photosApi.upload(url, photo)
suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) =
photosApi.save(body.map)
}
@@ -1,18 +1,19 @@
package com.meloda.fast.api.network.users package com.meloda.fast.data.users
import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkUser import com.meloda.fast.api.model.base.BaseVkUser
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.users.UsersUrls
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
interface UsersRepo { interface UsersApi {
@FormUrlEncoded @FormUrlEncoded
@POST(UsersUrls.GetById) @POST(UsersUrls.GetById)
suspend fun getById( suspend fun getById(
@FieldMap params: Map<String, String>? @FieldMap params: Map<String, String>?
): Answer<ApiResponse<List<BaseVkUser>>> ): ApiAnswer<ApiResponse<List<BaseVkUser>>>
} }
@@ -1,4 +1,4 @@
package com.meloda.fast.database.dao package com.meloda.fast.data.users
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@@ -0,0 +1,17 @@
package com.meloda.fast.data.users
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.users.UsersGetRequest
class UsersRepository(
private val usersApi: UsersApi,
private val usersDao: UsersDao
) {
suspend fun getById(params: UsersGetRequest) = usersApi.getById(params.map)
suspend fun storeUsers(users: List<VkUser>) {
usersDao.insert(users)
}
}
@@ -0,0 +1,26 @@
package com.meloda.fast.data.videos
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.videos.VideosSaveResponse
import com.meloda.fast.api.network.videos.VideosUploadResponse
import com.meloda.fast.api.network.videos.VideosUrls
import okhttp3.MultipartBody
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Url
interface VideosApi {
@POST(VideosUrls.Save)
suspend fun save(): ApiAnswer<ApiResponse<VideosSaveResponse>>
@Multipart
@POST
suspend fun upload(
@Url url: String,
@Part file: MultipartBody.Part
): ApiAnswer<VideosUploadResponse>
}
@@ -0,0 +1,13 @@
package com.meloda.fast.data.videos
import okhttp3.MultipartBody
class VideosRepository(
private val videosApi: VideosApi
) {
suspend fun save() = videosApi.save()
suspend fun upload(url: String, file: MultipartBody.Part) = videosApi.upload(url, file)
}
@@ -1,5 +1,6 @@
package com.meloda.fast.database package com.meloda.fast.database
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
@@ -7,27 +8,34 @@ import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.database.dao.GroupsDao import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.database.dao.UsersDao import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.model.AppAccount
@Database( @Database(
entities = [ entities = [
AppAccount::class,
VkConversation::class, VkConversation::class,
VkMessage::class, VkMessage::class,
VkUser::class, VkUser::class,
VkGroup::class VkGroup::class
], ],
version = 28, version = 34,
exportSchema = false, exportSchema = true,
autoMigrations = [
AutoMigration(from = 33, to = 34)
]
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun conversationsDao(): ConversationsDao abstract val accountsDao: AccountsDao
abstract fun messagesDao(): MessagesDao abstract val conversationsDao: ConversationsDao
abstract fun usersDao(): UsersDao abstract val messagesDao: MessagesDao
abstract fun groupsDao(): GroupsDao abstract val usersDao: UsersDao
abstract val groupsDao: GroupsDao
} }
@@ -4,6 +4,7 @@ import androidx.room.TypeConverter
import com.google.gson.Gson import com.google.gson.Gson
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.base.BaseVkMessage
import org.json.JSONObject import org.json.JSONObject
@Suppress("UnnecessaryVariable") @Suppress("UnnecessaryVariable")
@@ -13,6 +14,24 @@ class Converters {
private const val CACHE_SEPARATOR = "fastkruta228355" private const val CACHE_SEPARATOR = "fastkruta228355"
} }
@TypeConverter
fun fromGeoToString(geo: BaseVkMessage.Geo?): String? {
if (geo == null) return null
val string = Gson().toJson(geo)
return string
}
@TypeConverter
fun fromStringToGeo(string: String?): BaseVkMessage.Geo? {
if (string == null) return null
val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java)
return geo
}
@TypeConverter @TypeConverter
fun fromListVkMessageToString(messages: List<VkMessage>?): String? { fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
if (messages == null) return null if (messages == null) return null
@@ -49,7 +68,9 @@ class Converters {
fun fromStringToVkMessage(string: String?): VkMessage? { fun fromStringToVkMessage(string: String?): VkMessage? {
if (string == null) return null if (string == null) return null
return Gson().fromJson(string, VkMessage::class.java) val message = Gson().fromJson(string, VkMessage::class.java)
return message
} }
@TypeConverter @TypeConverter
@@ -82,7 +103,9 @@ class Converters {
fun fromVkAttachmentToString(attachment: VkAttachment?): String? { fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
if (attachment == null) return null if (attachment == null) return null
return Gson().toJson(attachment) val string = Gson().toJson(attachment)
return string
} }
@TypeConverter @TypeConverter
@@ -91,6 +114,8 @@ class Converters {
val className = JSONObject(string).optString("className") val className = JSONObject(string).optString("className")
return Gson().fromJson(string, Class.forName(className)) as VkAttachment? val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment?
return attachment
} }
} }
@@ -0,0 +1,103 @@
package com.meloda.fast.di
import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.data.account.AccountApi
import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.data.account.AccountsRepository
import com.meloda.fast.data.audios.AudiosApi
import com.meloda.fast.data.audios.AudiosRepository
import com.meloda.fast.data.auth.AuthApi
import com.meloda.fast.data.auth.AuthRepository
import com.meloda.fast.data.conversations.ConversationsApi
import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.data.conversations.ConversationsRepository
import com.meloda.fast.data.files.FilesApi
import com.meloda.fast.data.files.FilesRepository
import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.data.groups.GroupsRepository
import com.meloda.fast.data.messages.MessagesApi
import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.messages.MessagesRepository
import com.meloda.fast.data.photos.PhotosApi
import com.meloda.fast.data.photos.PhotosRepository
import com.meloda.fast.data.users.UsersApi
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.data.users.UsersRepository
import com.meloda.fast.data.videos.VideosApi
import com.meloda.fast.data.videos.VideosRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object DataModule {
@Singleton
@Provides
fun provideConversationsRepository(
conversationsApi: ConversationsApi,
conversationsDao: ConversationsDao
): ConversationsRepository = ConversationsRepository(conversationsApi, conversationsDao)
@Singleton
@Provides
fun provideMessagesRepository(
messagesApi: MessagesApi,
messagesDao: MessagesDao,
longPollApi: LongPollApi
): MessagesRepository = MessagesRepository(messagesApi, messagesDao, longPollApi)
@Singleton
@Provides
fun provideUsersRepository(
usersApi: UsersApi,
usersDao: UsersDao
): UsersRepository = UsersRepository(usersApi, usersDao)
@Singleton
@Provides
fun provideGroupsRepository(
groupsDao: GroupsDao
): GroupsRepository = GroupsRepository(groupsDao)
@Singleton
@Provides
fun provideAuthRepository(
authApi: AuthApi
): AuthRepository = AuthRepository(authApi)
@Singleton
@Provides
fun provideAccountsRepository(
accountApi: AccountApi,
accountsDao: AccountsDao
): AccountsRepository = AccountsRepository(accountApi, accountsDao)
@Singleton
@Provides
fun providePhotosRepository(
photosApi: PhotosApi
): PhotosRepository = PhotosRepository(photosApi)
@Singleton
@Provides
fun provideVideosRepository(
videosApi: VideosApi
): VideosRepository = VideosRepository(videosApi)
@Singleton
@Provides
fun provideAudiosRepository(
audiosApi: AudiosApi
): AudiosRepository = AudiosRepository(audiosApi)
@Singleton
@Provides
fun provideFilesRepository(
filesApi: FilesApi
): FilesRepository = FilesRepository(filesApi)
}
@@ -1,11 +1,12 @@
package com.meloda.fast.di package com.meloda.fast.di
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.data.account.AccountsDao
import com.meloda.fast.data.conversations.ConversationsDao
import com.meloda.fast.data.groups.GroupsDao
import com.meloda.fast.data.messages.MessagesDao
import com.meloda.fast.data.users.UsersDao
import com.meloda.fast.database.AppDatabase import com.meloda.fast.database.AppDatabase
import com.meloda.fast.database.dao.ConversationsDao
import com.meloda.fast.database.dao.GroupsDao
import com.meloda.fast.database.dao.MessagesDao
import com.meloda.fast.database.dao.UsersDao
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -23,22 +24,27 @@ object DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideUsersDao(appDatabase: AppDatabase): UsersDao = fun provideAccountsDao(appDatabase: AppDatabase): AccountsDao =
appDatabase.usersDao() appDatabase.accountsDao
@Provides @Provides
@Singleton @Singleton
fun provideConversationsDao(appDatabase: AppDatabase): ConversationsDao = fun provideConversationsDao(appDatabase: AppDatabase): ConversationsDao =
appDatabase.conversationsDao() appDatabase.conversationsDao
@Provides @Provides
@Singleton @Singleton
fun provideMessagesDao(appDatabase: AppDatabase): MessagesDao = fun provideMessagesDao(appDatabase: AppDatabase): MessagesDao =
appDatabase.messagesDao() appDatabase.messagesDao
@Provides
@Singleton
fun provideUsersDao(appDatabase: AppDatabase): UsersDao =
appDatabase.usersDao
@Provides @Provides
@Singleton @Singleton
fun provideGroupsDao(appDatabase: AppDatabase): GroupsDao = fun provideGroupsDao(appDatabase: AppDatabase): GroupsDao =
appDatabase.groupsDao() appDatabase.groupsDao
} }
@@ -1,24 +1,27 @@
package com.meloda.fast.di package com.meloda.fast.di
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.meloda.fast.api.LongPollUpdatesParser import com.meloda.fast.api.longpoll.LongPollUpdatesParser
import com.meloda.fast.api.network.AuthInterceptor import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory import com.meloda.fast.api.network.ResultCallFactory
import com.meloda.fast.api.network.account.AccountDataSource import com.meloda.fast.api.network.VkUrls
import com.meloda.fast.api.network.account.AccountRepo import com.meloda.fast.common.AppGlobal
import com.meloda.fast.api.network.auth.AuthDataSource import com.meloda.fast.common.UpdateManager
import com.meloda.fast.api.network.auth.AuthRepo import com.meloda.fast.data.account.AccountApi
import com.meloda.fast.api.network.conversations.ConversationsDataSource import com.meloda.fast.data.audios.AudiosApi
import com.meloda.fast.api.network.conversations.ConversationsRepo import com.meloda.fast.data.auth.AuthApi
import com.meloda.fast.api.network.longpoll.LongPollRepo import com.meloda.fast.data.conversations.ConversationsApi
import com.meloda.fast.api.network.messages.MessagesDataSource import com.meloda.fast.data.files.FilesApi
import com.meloda.fast.api.network.messages.MessagesRepo import com.meloda.fast.data.longpoll.LongPollApi
import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.data.messages.MessagesApi
import com.meloda.fast.api.network.users.UsersRepo import com.meloda.fast.data.messages.MessagesRepository
import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.data.ota.OtaApi
import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.data.photos.PhotosApi
import com.meloda.fast.database.dao.UsersDao import com.meloda.fast.data.users.UsersApi
import com.meloda.fast.data.videos.VideosApi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -34,18 +37,68 @@ import javax.inject.Singleton
@Module @Module
object NetworkModule { object NetworkModule {
/*
val chuckerCollector = ChuckerCollector(
context = this,
// Toggles visibility of the notification
showNotification = true,
// Allows to customize the retention period of collected data
retentionPeriod = RetentionManager.Period.ONE_HOUR
)
// Create the Interceptor
val chuckerInterceptor = ChuckerInterceptor.Builder(context)
// The previously created Collector
.collector(chuckerCollector)
// The max body content length in bytes, after this responses will be truncated.
.maxContentLength(250_000L)
// List of headers to replace with ** in the Chucker UI
.redactHeaders("Auth-Token", "Bearer")
// Read the whole response body even when the client does not consume the response completely.
// This is useful in case of parsing errors or when the response body
// is closed before being read like in Retrofit with Void and Unit types.
.alwaysReadResponseBody(true)
// Use decoder when processing request and response bodies. When multiple decoders are installed they
// are applied in an order they were added.
.addBodyDecoder(decoder)
// Controls Android shortcut creation. Available in SNAPSHOTS versions only at the moment
.createShortcut(true)
.build()
*/
@Singleton @Singleton
@Provides @Provides
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient = OkHttpClient.Builder() fun provideChuckerCollector(): ChuckerCollector =
ChuckerCollector(AppGlobal.Instance)
@Singleton
@Provides
fun provideChuckerInterceptor(
chuckerCollector: ChuckerCollector
): ChuckerInterceptor =
ChuckerInterceptor.Builder(AppGlobal.Instance)
.collector(chuckerCollector)
.build()
@Singleton
@Provides
fun provideOkHttpClient(
authInterceptor: AuthInterceptor,
chuckerInterceptor: ChuckerInterceptor
): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS) .connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(authInterceptor) .addInterceptor(authInterceptor)
.addInterceptor(chuckerInterceptor)
.followRedirects(true) .followRedirects(true)
.followSslRedirects(true) .followSslRedirects(true)
.addInterceptor(HttpLoggingInterceptor().apply { .addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY level = HttpLoggingInterceptor.Level.BODY
}).build() }
).build()
@Singleton @Singleton
@Provides @Provides
@@ -59,7 +112,7 @@ object NetworkModule {
client: OkHttpClient, client: OkHttpClient,
gson: Gson gson: Gson
): Retrofit = Retrofit.Builder() ): Retrofit = Retrofit.Builder()
.baseUrl("https://api.vk.com/") .baseUrl("${VkUrls.API}/")
.addConverterFactory(GsonConverterFactory.create(gson)) .addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(ResultCallFactory()) .addCallAdapterFactory(ResultCallFactory())
.client(client) .client(client)
@@ -71,73 +124,67 @@ object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun provideAuthRepo(retrofit: Retrofit): AuthRepo = fun provideAuthApi(retrofit: Retrofit): AuthApi =
retrofit.create(AuthRepo::class.java) retrofit.create(AuthApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo = fun provideConversationsApi(retrofit: Retrofit): ConversationsApi =
retrofit.create(ConversationsRepo::class.java) retrofit.create(ConversationsApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideUsersRepo(retrofit: Retrofit): UsersRepo = fun provideUsersApi(retrofit: Retrofit): UsersApi =
retrofit.create(UsersRepo::class.java) retrofit.create(UsersApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo = fun provideMessagesApi(retrofit: Retrofit): MessagesApi =
retrofit.create(MessagesRepo::class.java) retrofit.create(MessagesApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo = fun provideLongPollApi(retrofit: Retrofit): LongPollApi =
retrofit.create(LongPollRepo::class.java) retrofit.create(LongPollApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideAuthDataSource( fun provideLongPollUpdatesParser(messagesRepository: MessagesRepository): LongPollUpdatesParser =
repo: AuthRepo LongPollUpdatesParser(messagesRepository)
): AuthDataSource = AuthDataSource(repo)
@Provides @Provides
@Singleton @Singleton
fun provideUsersDataSource( fun provideAccountApi(retrofit: Retrofit): AccountApi =
repo: UsersRepo, retrofit.create(AccountApi::class.java)
dao: UsersDao
): UsersDataSource = UsersDataSource(repo, dao)
@Provides @Provides
@Singleton @Singleton
fun provideConversationsDataSource( fun provideOtaApi(retrofit: Retrofit): OtaApi =
repo: ConversationsRepo, retrofit.create(OtaApi::class.java)
dao: ConversationsDao
): ConversationsDataSource = ConversationsDataSource(repo, dao)
@Provides @Provides
@Singleton @Singleton
fun provideMessagesDataSource( fun provideUpdateManager(otaApi: OtaApi): UpdateManager =
messagesRepo: MessagesRepo, UpdateManager(otaApi)
messagesDao: MessagesDao,
longPollRepo: LongPollRepo
): MessagesDataSource = MessagesDataSource(
messagesRepo = messagesRepo,
messagesDao = messagesDao,
longPollRepo = longPollRepo
)
@Provides @Provides
@Singleton @Singleton
fun provideLongPollUpdatesParser(messagesDataSource: MessagesDataSource): LongPollUpdatesParser = fun providePhotosApi(retrofit: Retrofit): PhotosApi =
LongPollUpdatesParser(messagesDataSource) retrofit.create(PhotosApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideAccountRepo(retrofit: Retrofit): AccountRepo = fun provideVideosApi(retrofit: Retrofit): VideosApi =
retrofit.create(AccountRepo::class.java) retrofit.create(VideosApi::class.java)
@Provides @Provides
@Singleton @Singleton
fun provideAccountDataSource(repo: AccountRepo): AccountDataSource = fun provideAudiosApi(retrofit: Retrofit): AudiosApi =
AccountDataSource(repo) retrofit.create(AudiosApi::class.java)
@Provides
@Singleton
fun provideFilesApi(retrofit: Retrofit): FilesApi =
retrofit.create(FilesApi::class.java)
} }
@@ -2,17 +2,27 @@ package com.meloda.fast.extensions
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.graphics.drawable.Drawable
import android.os.Parcelable import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.SparseArray import android.util.SparseArray
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.Px import androidx.annotation.Px
import androidx.annotation.StyleRes import androidx.appcompat.widget.Toolbar
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach
import androidx.lifecycle.MutableLiveData
import com.google.common.net.MediaType
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
fun Int.dpToPx(): Int { fun Int.dpToPx(): Int {
val metrics = Resources.getSystem().displayMetrics val metrics = Resources.getSystem().displayMetrics
@@ -52,6 +62,11 @@ fun ValueAnimator.startWithIntValues(from: Int, to: Int) {
start() start()
} }
fun ValueAnimator.startWithFloatValues(from: Float, to: Float) {
setFloatValues(from, to)
start()
}
fun View.setMarginsPx( fun View.setMarginsPx(
@Px leftMargin: Int? = null, @Px leftMargin: Int? = null,
@Px topMargin: Int? = null, @Px topMargin: Int? = null,
@@ -85,3 +100,86 @@ fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE)
fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) { fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse
} }
fun View.showKeyboard(flags: Int = 0) {
AppGlobal.inputMethodManager.showSoftInput(this, flags)
}
fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) {
AppGlobal.inputMethodManager.hideSoftInputFromWindow(
focusedView?.windowToken ?: this.windowToken, flags
)
}
fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) {
menu.forEach { item ->
item.icon?.setTint(colorToTint)
}
}
fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem {
val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate(
LayoutInflater.from(context), null, false
)
val avatarMenuItem = menu.add("Profile")
avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
avatarMenuItem.actionView = avatarMenuItemBinding.root
val imageView = avatarMenuItemBinding.avatar
when {
urlToLoad != null -> {
imageView.loadWithGlide(
url = urlToLoad,
transformations = ImageLoader.userAvatarTransformations
)
}
drawable != null -> {
imageView.loadWithGlide(
drawable = drawable,
transformations = ImageLoader.userAvatarTransformations
)
}
}
return avatarMenuItem
}
fun <T> MutableLiveData<T>.notifyObservers() {
this.value = this.value
}
fun <T> MutableLiveData<T>.setIfNotEquals(item: T) {
if (this.value != item) this.value = item
}
fun <T> MutableLiveData<T>.requireValue(): T {
return this.value!!
}
val EditText.trimmedText: String get() = text.toString().trim()
val MediaType.mimeType: String get() = "${type()}/${subtype()}"
fun EditText.selectLast() {
setSelection(text.length)
}
fun <T> T?.requireNotNull(): T {
return requireNotNull(this)
}
fun String?.orDots(count: Int = 3): String {
return this ?: ("." * count)
}
private operator fun String.times(count: Int): String {
val builder = StringBuilder()
for (i in 0 until count) {
builder.append(this)
}
return builder.toString()
}

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