Upstream changes (#23)

This commit is contained in:
2024-07-11 02:12:32 +03:00
committed by GitHub
parent 8a6378f509
commit 3503ecffab
906 changed files with 23577 additions and 24115 deletions
+48
View File
@@ -0,0 +1,48 @@
name: Android CI
on:
pull_request:
branches: [ "dev" ]
push:
branches: [ "dev" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs:
build_apk_aab:
runs-on: ubuntu-latest
name: Build dev artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign debug APKs
run: ./gradlew assembleDebug
- name: Build and sign release APKs
run: ./gradlew assembleRelease
- name: Upload dev-debug APK
uses: actions/upload-artifact@v4
with:
name: app-dev-debug.apk
path: app/build/outputs/apk/amethyst/debug/app-amethyst-debug.apk
- name: Upload dev-release APK
uses: actions/upload-artifact@v4
with:
name: app-dev-release.apk
path: app/build/outputs/apk/amethyst/release/app-amethyst-release.apk
+47
View File
@@ -0,0 +1,47 @@
name: Android CI
on:
push:
branches: [ "master" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs:
build_apk_aab:
runs-on: ubuntu-latest
name: Build full artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign debug APKs
run: ./gradlew assembleDebug
- name: Build and sign release APKs
run: ./gradlew assembleRelease
- name: Upload full-debug APK
uses: actions/upload-artifact@v4
with:
name: app-full-debug.apk
path: app/build/outputs/apk/full/debug/app-full-debug.apk
- name: Upload full-release APK
uses: actions/upload-artifact@v4
with:
name: app-full-release.apk
path: app/build/outputs/apk/full/release/app-full-release.apk
+2 -1
View File
@@ -8,9 +8,10 @@
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build build/
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
.idea .idea
/.kotlin
+79
View File
@@ -1,3 +1,82 @@
### Module Graph
```mermaid
%%{
init: {
'theme': 'base',
'themeVariables': {"primaryTextColor":"#fff","primaryColor":"#5a4f7c","primaryBorderColor":"#5a4f7c","lineColor":"#f5a623","tertiaryColor":"#40375c","fontSize":"12px"}
}
}%%
graph LR
subgraph :core
:core:database["database"]
:core:model["model"]
:core:data["data"]
:core:ui["ui"]
:core:common["common"]
:core:designsystem["designsystem"]
:core:datastore["datastore"]
:core:network["network"]
end
subgraph :feature
:feature:chatmaterials["chatmaterials"]
:feature:messageshistory["messageshistory"]
:feature:settings["settings"]
:feature:languagepicker["languagepicker"]
:feature:userbanned["userbanned"]
:feature:auth["auth"]
:feature:conversations["conversations"]
:feature:photoviewer["photoviewer"]
end
:core:database --> :core:model
:feature:chatmaterials --> :core:data
:feature:chatmaterials --> :core:model
:feature:chatmaterials --> :core:ui
:feature:messageshistory --> :core:data
:feature:messageshistory --> :core:model
:feature:messageshistory --> :core:ui
:feature:settings --> :core:data
:feature:settings --> :core:model
:feature:settings --> :core:ui
:feature:languagepicker --> :core:data
:feature:languagepicker --> :core:model
:feature:languagepicker --> :core:ui
:feature:userbanned --> :core:data
:feature:userbanned --> :core:model
:feature:userbanned --> :core:ui
:app --> :feature:auth
:app --> :feature:chatmaterials
:app --> :feature:conversations
:app --> :feature:languagepicker
:app --> :feature:messageshistory
:app --> :feature:photoviewer
:app --> :feature:settings
:app --> :feature:userbanned
:app --> :core:common
:app --> :core:ui
:app --> :core:designsystem
:app --> :core:data
:app --> :core:model
:app --> :core:datastore
:core:data --> :core:common
:core:data --> :core:model
:core:data --> :core:network
:core:data --> :core:database
:core:network --> :core:common
:core:network --> :core:model
:feature:auth --> :core:data
:feature:auth --> :core:ui
:core:ui --> :core:designsystem
:core:ui --> :core:model
:feature:photoviewer --> :core:data
:feature:photoviewer --> :core:model
:feature:photoviewer --> :core:ui
:feature:conversations --> :core:data
:feature:conversations --> :core:model
:feature:conversations --> :core:ui
:core:datastore --> :core:common
```
# fast-messenger # fast-messenger
Unofficial messenger for russian social network VKontakte Unofficial messenger for russian social network VKontakte
+2
View File
@@ -1 +1,3 @@
/build /build
/keystore/keystore.properties
/full
+122 -164
View File
@@ -1,226 +1,184 @@
@file:Suppress("UnstableApiUsage") import java.util.Properties
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"")
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"")
val msAppCenterToken: String =
gradleLocalProperties(rootDir).getProperty("msAppCenterAppToken", "\"\"")
val otaSecretCode: String = gradleLocalProperties(rootDir).getProperty("otaSecretCode", "\"\"")
val majorVersion = 1
val minorVersion = 6
val patchVersion = 4
plugins { plugins {
id("com.android.application") alias(libs.plugins.com.android.application)
id("kotlin-android") alias(libs.plugins.org.jetbrains.kotlin.android)
id("kotlin-kapt") alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
id("kotlin-parcelize") alias(libs.plugins.com.google.devtools.ksp)
id("org.jetbrains.kotlin.android") alias(libs.plugins.kotlin.compose.compiler)
id("com.google.devtools.ksp") alias(libs.plugins.kotlin.serialization)
} }
android { android {
namespace = "com.meloda.fast" namespace = "com.meloda.app.fast"
compileSdk = Configs.compileSdk
compileSdk = 34
applicationVariants.all {
outputs.all {
(this as BaseVariantOutputImpl).outputFileName =
"${name}-${versionName}-${versionCode}.apk"
}
}
defaultConfig { defaultConfig {
applicationId = "com.meloda.fast" applicationId = "com.meloda.app.fast"
minSdk = 24 minSdk = Configs.minSdk
targetSdk = 34 targetSdk = Configs.targetSdk
versionCode = 1 versionCode = Configs.appCode
versionName = "alpha" versionName = Configs.appName
javaCompileOptions { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
annotationProcessorOptions {
// arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
} }
// TODO: 06/05/2024, Danil Nikolaev: придумать, как совместить с github actions
// applicationVariants.all {
// val variant = this
// variant.outputs
// .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
// .forEach { output ->
// if (variant.buildType.name == "release") {
// val outputFileName = "fastvk-v${variant.versionName}-${variant.flavorName}.apk"
// output.outputFileName = outputFileName
// }
// }
// }
signingConfigs {
create("release") {
val keystoreProperties = Properties()
val keystorePropertiesFile = file("keystore/keystore.properties")
storeFile = file("keystore/keystore.jks")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().let(keystoreProperties::load)
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
} else {
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("RELEASE_SIGN_KEY_ALIAS")
keyPassword = System.getenv("RELEASE_SIGN_KEY_PASSWORD")
}
}
create("debugSigning") {
initWith(getByName("release"))
} }
} }
buildTypes { buildTypes {
getByName("debug") { named("debug") {
buildConfigField("String", "sdkPackage", sdkPackage) signingConfig = signingConfigs.getByName("debugSigning")
buildConfigField("String", "sdkFingerprint", sdkFingerprint) applicationIdSuffix = ".debug"
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
buildConfigField("String", "otaSecretCode", otaSecretCode)
versionNameSuffix = "_${getVersionName()}"
} }
getByName("release") { named("release") {
isMinifyEnabled = false signingConfig = signingConfigs.getByName("release")
buildConfigField("String", "sdkPackage", sdkPackage) isMinifyEnabled = true
buildConfigField("String", "sdkFingerprint", sdkFingerprint) isShrinkResources = true
buildConfigField("String", "msAppCenterAppToken", msAppCenterToken)
buildConfigField("String", "otaSecretCode", otaSecretCode)
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
) )
} }
// TODO: 15/05/2024, Danil Nikolaev: add to other modules with build convention
register("staging") {
initWith(getByName("release"))
applicationIdSuffix = ".staging"
}
} }
val flavorDimension = "version" val flavorDimension = "variant"
flavorDimensions += flavorDimension flavorDimensions += flavorDimension
productFlavors { productFlavors {
create("dev") { register("amethyst") {
resourceConfigurations += listOf("en", "xxhdpi")
dimension = flavorDimension
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
}
create("full") {
dimension = flavorDimension dimension = flavorDimension
isDefault = true
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = Configs.java
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = Configs.java
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString() jvmTarget = Configs.java.toString()
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
} }
buildFeatures { buildFeatures {
viewBinding = true
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.4.5"
useLiveLiterals = true useLiveLiterals = true
} }
packagingOptions {
jniLibs {
useLegacyPackaging = false
}
}
}
kapt { // packaging {
correctErrorTypes = true // resources {
// excludes += "/META-INF/{AL2.0,LGPL2.1}"
// }
// }
} }
fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion"
val currentTime get() = (System.currentTimeMillis() / 1000).toInt()
dependencies { dependencies {
implementation(projects.feature.auth)
implementation(projects.feature.chatmaterials)
implementation(projects.feature.conversations)
implementation(projects.feature.languagepicker)
implementation(projects.feature.messageshistory)
implementation(projects.feature.photoviewer)
implementation(projects.feature.settings)
implementation(projects.feature.friends)
implementation(projects.feature.profile)
implementation(projects.core.common)
implementation(projects.core.ui)
implementation(projects.core.designsystem)
implementation(projects.core.data)
implementation(projects.core.model)
implementation(projects.core.datastore)
// DI zone // Tests zone
implementation("io.insert-koin:koin-android:3.4.0") testImplementation(libs.junit)
// end of DI zone // end of Tests zone
implementation("com.github.skydoves:cloudy:0.1.2") // Compose-Bom zone
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
// end of Compose-Bom zone
implementation("io.coil-kt:coil-compose:2.3.0") implementation(libs.accompanist.permissions)
implementation("io.coil-kt:coil:2.3.0")
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2") // Coil for Compose
implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2") implementation(libs.coil.compose)
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.21") androidTestImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.test.manifest)
debugImplementation(libs.compose.ui.tooling)
implementation("androidx.core:core-ktx:1.10.1") implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.compose.navigation)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation(libs.coil)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-splashscreen:1.0.1") implementation(libs.core.ktx)
implementation("androidx.appcompat:appcompat:1.6.1") implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation("androidx.activity:activity-ktx:1.7.2") implementation(libs.preference.ktx)
implementation(libs.material)
implementation("androidx.fragment:fragment-ktx:1.6.1") implementation(libs.haze)
implementation(libs.haze.materials)
implementation("androidx.preference:preference-ktx:1.2.0") implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation(libs.nanokt)
implementation(libs.nanokt.android)
implementation(libs.nanokt.jvm)
implementation("androidx.recyclerview:recyclerview:1.3.1") implementation(libs.androidx.navigation.compose)
implementation(libs.kotlin.serialization)
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0")
implementation("androidx.room:room-ktx:2.5.2")
implementation("androidx.room:room-runtime:2.5.2")
ksp("androidx.room:room-compiler:2.5.2")
implementation("com.github.terrakok:cicerone:7.1")
implementation("com.github.massoudss:waveformSeekBar:5.0.0")
implementation("com.github.bumptech.glide:glide:4.15.1")
ksp("com.github.bumptech.glide:compiler:4.15.1")
implementation("com.github.fondesa:kpermissions:3.4.0")
implementation("com.github.fondesa:kpermissions-coroutines:3.4.0")
implementation("com.microsoft.appcenter:appcenter-analytics:5.0.1")
implementation("com.microsoft.appcenter:appcenter-crashes:5.0.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.7.1")
implementation("com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.guava:guava:31.1-jre")
implementation("com.google.android.material:material:1.9.0")
implementation("com.github.chuckerteam.chucker:library:3.5.2")
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
// Compose zone
implementation(platform("androidx.compose:compose-bom:2023.04.01"))
implementation("androidx.compose.material3:material3:1.1.1")
// implementation("androidx.compose.material:material:1.4.3")
implementation("androidx.compose.ui:ui:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
implementation("androidx.compose.material3:material3-window-size-class:1.1.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.compose.runtime:runtime-saveable:1.6.0-alpha02")
// end of Compose zone
} }
Binary file not shown.
@@ -0,0 +1,52 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "3ebd234270e36902d3d461af38664869",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` 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
},
{
"fieldPath": "trustedHash",
"columnName": "trustedHash",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId"
]
},
"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, '3ebd234270e36902d3d461af38664869')"
]
}
}
@@ -0,0 +1,424 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6a940f719e8dd56ea5c196c152f1e536",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` 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": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `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": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"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, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` 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": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastConversationMessageId",
"columnName": "lastConversationMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"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": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"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, '6a940f719e8dd56ea5c196c152f1e536')"
]
}
}
@@ -0,0 +1,46 @@
package com.meloda.app.fast.tests
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
class LoginSignInTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun signInButtonIsClickable() {
composeTestRule.setContent {
// LogoScreen(onAction = {})
}
composeTestRule.onNodeWithTag(testTag = "Sign in button").assertHasClickAction()
}
@Test
fun signInButtonTriggersSignInAction() {
var signInClicked = true
composeTestRule.setContent {
// com.meloda.fast.auth.login.presentation.LogoScreen(
// onAction = { action ->
// when (action) {
// UiAction.NextClicked -> {
// signInClicked = true
// }
//
// else -> Unit
// }
// }
// )
}
composeTestRule.onNodeWithTag("Sign in button").performClick()
assert(signInClicked)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#F44336</color>
<string name="app_name">Fast Dev</string>
</resources> </resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Fast Debug</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

+9 -55
View File
@@ -1,6 +1,5 @@
<?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">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -9,29 +8,22 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application <application
android:name=".common.AppGlobal" android:name="com.meloda.app.fast.common.AppGlobal"
android:allowBackup="false" android:allowBackup="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:supportsRtl="false" android:networkSecurityConfig="@xml/network_security_config"
android:testOnly="false" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme">
android:usesCleartextTraffic="true"
tools:ignore="DataExtractionRules"
tools:replace="android:allowBackup"
tools:targetApi="tiramisu">
<activity <activity
android:name=".screens.main.activity.MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -39,10 +31,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".screens.testing.TestActivity" />
<service <service
android:name=".service.LongPollService" android:name=".service.longpolling.LongPollingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
@@ -53,33 +43,6 @@
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
android:name=".service.LongPollQSTileService"
android:enabled="true"
android:exported="true"
android:icon="@drawable/ic_round_settings_24"
android:label="Open settings"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.MyCustomControlService"
android:exported="true"
android:label="Fast"
android:permission="android.permission.BIND_CONTROLS">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" android:enabled="false"
@@ -89,15 +52,6 @@
android:value="true" /> android:value="true" />
</service> </service>
<receiver
android:name=".receiver.StopLongPollServiceReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.meloda.fast.receiver.ACTION_STOP" />
</intent-filter>
</receiver>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -0,0 +1,254 @@
package com.meloda.app.fast
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.extensions.isSdkAtLeast
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.datastore.model.ThemeConfig
import com.meloda.app.fast.designsystem.AppTheme
import com.meloda.app.fast.designsystem.CheckPermission
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.RequestPermission
import com.meloda.app.fast.model.MainScreenState
import com.meloda.app.fast.service.OnlineService
import com.meloda.app.fast.service.longpolling.LongPollingService
import org.koin.compose.KoinContext
import org.koin.compose.koinInject
import com.meloda.app.fast.designsystem.R as UiR
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
createNotificationChannels()
setContent {
KoinContext {
val userSettings: UserSettings = koinInject()
LaunchedEffect(true) {
userSettings.updateUsingDarkTheme()
}
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
toggleLongPollService(true, isLongPollInBackground)
val isOnline by userSettings.online.collectAsStateWithLifecycle()
LifecycleResumeEffect(isOnline) {
toggleOnlineService(isOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val theme by userSettings.theme.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalTheme provides ThemeConfig(
usingDarkStyle = theme.usingDarkStyle,
usingDynamicColors = theme.usingDynamicColors,
selectedColorScheme = theme.selectedColorScheme,
usingAmoledBackground = theme.usingAmoledBackground,
usingBlur = theme.usingBlur,
multiline = theme.multiline
)
) {
AppTheme(
useDarkTheme = LocalTheme.current.usingDarkStyle,
useDynamicColors = LocalTheme.current.usingDynamicColors,
selectedColorScheme = LocalTheme.current.selectedColorScheme,
useAmoledBackground = LocalTheme.current.usingAmoledBackground,
) {
RootGraph()
}
}
}
}
}
private fun createNotificationChannels() {
isSdkAtLeast(Build.VERSION_CODES.O) {
val dialogsName = "Dialogs"
val dialogsDescriptionText = "Channel for dialogs notifications"
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
val dialogsChannel =
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
description = dialogsDescriptionText
}
val longPollName = "Long Polling"
val longPollDescriptionText = "Channel for long polling service"
val longPollImportance = NotificationManager.IMPORTANCE_NONE
val longPollChannel =
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
description = longPollDescriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
}
}
private fun toggleLongPollService(
enable: Boolean,
asForeground: Boolean = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
) {
if (enable) {
val longPollIntent = Intent(this, LongPollingService::class.java)
if (asForeground) {
ContextCompat.startForegroundService(this, longPollIntent)
} else {
startService(longPollIntent)
}
} else {
stopService(Intent(this, LongPollingService::class.java))
}
}
private fun toggleOnlineService(enable: Boolean) {
if (enable) {
startService(Intent(this, OnlineService::class.java))
} else {
stopService(Intent(this, OnlineService::class.java))
}
}
private fun stopServices() {
toggleOnlineService(enable = false)
val asForeground = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
if (!asForeground) {
toggleLongPollService(enable = false)
}
}
private fun setNewLanguage(newLanguage: String) {
val appLocales = AppCompatDelegate.getApplicationLocales()
if (newLanguage.isEmpty()) {
if (!appLocales.isEmpty) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
}
} else {
if (!appLocales.toLanguageTags().startsWith(newLanguage)) {
val newLocale = LocaleListCompat.forLanguageTags(newLanguage)
AppCompatDelegate.setApplicationLocales(newLocale)
}
}
}
override fun onDestroy() {
super.onDestroy()
stopServices()
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NotificationsPermissionChecker(
screenState: MainScreenState,
viewModel: MainViewModel
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val permission =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
if (screenState.isNeedToOpenAppPermissions) {
viewModel.onAppPermissionsOpened()
LocalContext.current.apply {
startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", this.packageName, null)
)
)
}
}
if (screenState.isNeedToRequestNotifications) {
RequestPermission(permission = permission)
}
val isNeedToCheckNotificationsPermission by remember {
derivedStateOf {
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
))
}
}
if (isNeedToCheckNotificationsPermission) {
CheckPermission(
showRationale = {
MaterialDialog(
title = UiText.Resource(UiR.string.warning),
text = UiText.Simple("The application will not be able to work properly without permission to send notifications."),
confirmText = UiText.Simple("Grant"),
confirmAction = {
viewModel.onRequestNotificationsPermissionClicked(true)
},
cancelText = UiText.Resource(UiR.string.cancel),
cancelAction = viewModel::onNotificationsAlertNegativeClicked,
onDismissAction = viewModel::onNotificationsAlertNegativeClicked,
buttonsInvokeDismiss = false
)
},
onDenied = {
MaterialDialog(
title = UiText.Resource(UiR.string.warning),
text = UiText.Simple("The application needs permission to send notifications to update messages and other information."),
confirmText = UiText.Simple("Grant"),
confirmAction = {
viewModel.onRequestNotificationsPermissionClicked(false)
},
cancelText = UiText.Resource(UiR.string.cancel),
onDismissAction = {},
buttonsInvokeDismiss = false
)
},
permission = permission
)
}
}
@@ -0,0 +1,196 @@
package com.meloda.app.fast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.conversations.navigation.Conversations
import com.meloda.app.fast.conversations.navigation.conversationsRoute
import com.meloda.app.fast.friends.navigation.Friends
import com.meloda.app.fast.friends.navigation.friendsRoute
import com.meloda.app.fast.model.BaseError
import kotlinx.serialization.Serializable
import com.meloda.app.fast.designsystem.R as UiR
@Serializable
object MainGraph
@Serializable
object Main
@Serializable
object Profile
data class BottomNavigationItem(
val titleResId: Int,
val selectedIconResId: Int,
val unselectedIconResId: Int,
val route: Any,
)
@OptIn(ExperimentalMaterial3Api::class)
fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToMessagesHistory: (conversationId: Int) -> Unit,
) {
val items = listOf(
BottomNavigationItem(
titleResId = UiR.string.title_friends,
selectedIconResId = UiR.drawable.baseline_people_alt_24,
unselectedIconResId = UiR.drawable.outline_people_alt_24,
route = Friends,
),
BottomNavigationItem(
titleResId = UiR.string.title_conversations,
selectedIconResId = UiR.drawable.baseline_chat_24,
unselectedIconResId = UiR.drawable.outline_chat_24,
route = Conversations
),
BottomNavigationItem(
titleResId = UiR.string.title_profile,
selectedIconResId = UiR.drawable.baseline_account_circle_24,
unselectedIconResId = UiR.drawable.outline_account_circle_24,
route = Profile
)
)
val routes = items.map(BottomNavigationItem::route)
composable<Main> {
val navController = rememberNavController()
var selectedItemIndex by rememberSaveable {
mutableIntStateOf(1)
}
var isBottomBarVisible by rememberSaveable {
mutableStateOf(true)
}
Scaffold(
bottomBar = {
AnimatedVisibility(
visible = isBottomBarVisible,
enter = slideIn { IntOffset(0, 400) },
exit = slideOut { IntOffset(0, 400) }
) {
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
selected = selectedItemIndex == index,
onClick = {
if (selectedItemIndex != index) {
val currentRoute = routes[selectedItemIndex]
selectedItemIndex = index
navController.navigate(item.route) {
popUpTo(route = currentRoute) {
inclusive = true
}
}
}
},
icon = {
Icon(
painter = painterResource(
id = if (selectedItemIndex == index) item.selectedIconResId
else item.unselectedIconResId
),
contentDescription = null
)
},
alwaysShowLabel = false
)
}
}
}
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding.calculateBottomPadding())
) {
NavHost(
navController = navController,
startDestination = MainGraph,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
navigation<MainGraph>(startDestination = Conversations) {
friendsRoute(
onError = onError,
navController = navController
)
conversationsRoute(
onError = onError,
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
navController = navController,
onListScrollingUp = { isScrolling ->
// isBottomBarVisible = isScrolling
}
)
composable<Profile> {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = stringResource(id = UiR.string.title_profile))
},
actions = {
IconButton(onClick = onNavigateToSettings) {
Icon(
imageVector = Icons.Rounded.Settings,
contentDescription = null
)
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,147 @@
package com.meloda.app.fast
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.common.extensions.updateValue
import com.meloda.app.fast.data.db.AccountsRepository
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.LongPollState
import com.meloda.app.fast.model.MainScreenState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
interface MainViewModel {
val screenState: StateFlow<MainScreenState>
val longPollState: StateFlow<LongPollState>
val startOnlineService: StateFlow<Boolean>
fun useDynamicColorsChanged(use: Boolean)
fun useDarkThemeChanged(use: Boolean)
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
fun onNotificationsAlertNegativeClicked()
fun onNotificationsRequested()
fun onAppPermissionsOpened()
fun onError(error: BaseError)
fun onAuthOpened()
}
class MainViewModelImpl(
private val accountsRepository: AccountsRepository,
private val userSettings: UserSettings
) : MainViewModel, ViewModel() {
init {
loadAccounts()
}
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
override val longPollState = MutableStateFlow(
if (SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
) {
LongPollState.ForegroundService
} else {
LongPollState.DefaultService
}
)
override val startOnlineService = MutableStateFlow(
SettingsController.getBoolean(
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
)
)
override fun useDynamicColorsChanged(use: Boolean) {
screenState.updateValue(screenState.value.copy(useDynamicColors = use))
}
override fun useDarkThemeChanged(use: Boolean) {
screenState.updateValue(screenState.value.copy(useDarkTheme = use))
}
override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) {
screenState.setValue { old ->
if (fromRationale) {
old.copy(isNeedToOpenAppPermissions = true)
} else {
old.copy(isNeedToRequestNotifications = true)
}
}
}
override fun onNotificationsAlertNegativeClicked() {
SettingsController.edit {
putBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
false
)
}
userSettings.setLongPollBackground(false)
}
override fun onNotificationsRequested() {
screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) }
}
override fun onAppPermissionsOpened() {
screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) }
}
override fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired -> {
screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) }
}
}
}
override fun onAuthOpened() {
screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) }
}
private fun loadAccounts() {
viewModelScope.launch(Dispatchers.IO) {
val accounts = accountsRepository.getAccounts()
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
if (accounts.isNotEmpty()) {
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
if (currentAccount != null) {
UserConfig.apply {
this.userId = currentAccount.userId
this.accessToken = currentAccount.accessToken
this.fastToken = currentAccount.fastToken
this.trustedHash = currentAccount.trustedHash
}
}
}
screenState.setValue { old ->
old.copy(
accounts = accounts,
accountsLoaded = true
)
}
}
}
}
@@ -0,0 +1,92 @@
package com.meloda.app.fast
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.auth.AuthGraph
import com.meloda.app.fast.auth.authNavGraph
import com.meloda.app.fast.auth.navigateToAuth
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute
import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory
import com.meloda.app.fast.settings.presentation.navigateToSettings
import com.meloda.app.fast.settings.presentation.settingsRoute
import org.koin.androidx.compose.koinViewModel
@Composable
fun RootGraph(navController: NavHostController = rememberNavController()) {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
if (screenState.isNeedToOpenAuth) {
viewModel.onAuthOpened()
navController.navigateToAuth(clearBackStack = true)
}
if (screenState.accountsLoaded) {
val isNeedToShowConversations by remember {
derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() }
}
NavHost(
navController = navController,
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
authNavGraph(
onError = viewModel::onError,
onNavigateToMain = navController::navigateToMain,
navController = navController
)
mainScreen(
onError = viewModel::onError,
onNavigateToSettings = navController::navigateToSettings,
onNavigateToMessagesHistory = navController::navigateToMessagesHistory
)
messagesHistoryRoute(
onError = viewModel::onError,
onBack = navController::navigateUp,
onNavigateToChatAttachments = navController::navigateToChatMaterials
)
chatMaterialsRoute(
onBack = navController::navigateUp
)
settingsRoute(
onError = viewModel::onError,
onBack = navController::navigateUp,
onNavigateToAuth = { navController.navigateToAuth(true) },
onNavigateToLanguagePicker = navController::navigateToLanguagePicker
)
languagePickerRoute(onBack = navController::navigateUp)
}
}
NotificationsPermissionChecker(
screenState = screenState,
viewModel = viewModel
)
}
fun NavController.navigateToMain() {
this.navigate(Main) {
popUpTo(0) {
inclusive = true
}
}
}
@@ -0,0 +1,35 @@
package com.meloda.app.fast.common
import android.app.Application
import androidx.preference.PreferenceManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.meloda.app.fast.common.di.applicationModule
import com.meloda.app.fast.datastore.SettingsController
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
class AppGlobal : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
SettingsController.init(preferences)
UserConfig.init(preferences)
initKoin()
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(this@AppGlobal)
modules(applicationModule)
}
}
override fun newImageLoader(): ImageLoader = get()
}
@@ -0,0 +1,47 @@
package com.meloda.app.fast.common.di
import android.content.Context
import android.content.res.Resources
import android.os.PowerManager
import androidx.preference.PreferenceManager
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.auth.authModule
import com.meloda.app.fast.conversations.di.conversationsModule
import com.meloda.app.fast.data.di.dataModule
import com.meloda.app.fast.friends.di.friendsModule
import com.meloda.app.fast.languagepicker.di.languagePickerModule
import com.meloda.app.fast.messageshistory.di.messagesHistoryModule
import com.meloda.app.fast.photoviewer.di.photoViewModule
import com.meloda.app.fast.profile.di.profileModule
import com.meloda.app.fast.service.longpolling.di.longPollModule
import com.meloda.app.fast.settings.di.settingsModule
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.qualifier
import org.koin.dsl.module
val applicationModule = module {
includes(dataModule)
includes(
authModule,
conversationsModule,
settingsModule,
messagesHistoryModule,
photoViewModule,
languagePickerModule,
longPollModule,
friendsModule,
profileModule
)
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
viewModelOf(::MainViewModelImpl) {
qualifier = qualifier("main")
}
}
@@ -0,0 +1,7 @@
package com.meloda.app.fast.model
sealed class LongPollState {
data object ForegroundService : LongPollState()
data object DefaultService : LongPollState()
data object Stop : LongPollState()
}
@@ -0,0 +1,30 @@
package com.meloda.app.fast.model
import androidx.compose.runtime.Immutable
import com.meloda.app.fast.model.database.AccountEntity
@Immutable
data class MainScreenState(
val accounts: List<AccountEntity>,
val accountsLoaded: Boolean,
val useDarkTheme: Boolean,
val useDynamicColors: Boolean,
val isNeedToRequestNotifications: Boolean,
val isNeedToOpenAppPermissions: Boolean,
val isNeedToOpenAuth: Boolean,
) {
companion object {
val EMPTY: MainScreenState = MainScreenState(
accounts = emptyList(),
accountsLoaded = false,
// TODO: 05/05/2024, Danil Nikolaev: implement
useDarkTheme = false,
useDynamicColors = false,
isNeedToRequestNotifications = false,
isNeedToOpenAppPermissions = false,
isNeedToOpenAuth = false,
)
}
}
@@ -0,0 +1,7 @@
package com.meloda.app.fast.model
sealed class ServicesState {
data object Started : ServicesState()
data object Stopped : ServicesState()
data object Unknown : ServicesState()
}
@@ -1,4 +1,4 @@
package com.meloda.fast.receiver package com.meloda.app.fast.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@@ -0,0 +1,129 @@
package com.meloda.app.fast.service
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.common.extensions.createTimerFlow
import com.meloda.app.fast.data.api.account.AccountUseCase
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.minutes
class OnlineService : Service() {
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "error: $throwable")
throwable.printStackTrace()
}
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job + exceptionHandler
private val coroutineScope = CoroutineScope(coroutineContext)
private val useCase: AccountUseCase by inject()
private var timerJob: Job? = null
private var onlineJob: Job? = null
override fun onBind(intent: Intent?): IBinder? {
Log.d(STATE_TAG, "onBind: intent: $intent")
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY
Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this")
// TODO: 05/05/2024, Danil Nikolaev: implement
// if (AppGlobal.preferences.getBoolean(
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
// )
// ) {
// createTimer()
// }
return START_STICKY
}
private fun createTimer() {
timerJob = createTimerFlow(
isNeedToEndCondition = { false },
onStartAction = ::setOnline,
onTickAction = ::setOnline,
interval = 5.minutes
).launchIn(coroutineScope)
}
private fun setOnline() {
if (onlineJob != null) return
// TODO: 05/05/2024, Danil Nikolaev: implement
// if (!AppGlobal.preferences.getBoolean(
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
// )
// ) return
Log.d(TAG, "setOnline()")
onlineJob = coroutineScope.launch {
val token = UserConfig.fastToken ?: UserConfig.accessToken
if (token.isBlank()) {
Log.d(TAG, "setOnline: token is empty")
return@launch
}
val response = useCase.setOnline(
voip = false,
accessToken = token
)
Log.d(TAG, "setOnline: response: $response")
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
}
private suspend fun setOffline() {
Log.d(TAG, "setOffline()")
val response = useCase.setOffline(
accessToken = UserConfig.accessToken
)
Log.d(TAG, "setOffline: response: $response")
}
override fun onLowMemory() {
Log.d(STATE_TAG, "onLowMemory")
super.onLowMemory()
}
override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy")
timerJob?.cancel("OnlineService destroyed")
onlineJob?.cancel("OnlineService destroyed")
super.onDestroy()
}
companion object {
private const val TAG = "OnlineService"
private const val STATE_TAG = "OnlineServiceState"
}
}
@@ -0,0 +1,271 @@
package com.meloda.app.fast.service.longpolling
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.conena.nanokt.android.app.stopForegroundCompat
import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.common.VkConstants
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.data.LongPollUpdatesParser
import com.meloda.app.fast.data.LongPollUseCase
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
import com.meloda.app.fast.model.api.data.LongPollUpdates
import com.meloda.app.fast.model.api.data.VkLongPollData
import com.meloda.app.fast.util.NotificationsUtils
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LongPollingService : Service() {
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e(TAG, "error: $throwable")
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
}
private val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job + exceptionHandler
private val coroutineScope = CoroutineScope(coroutineContext)
private val longPollUseCase: LongPollUseCase by inject()
private val updatesParser: LongPollUpdatesParser by inject()
private val preferences: SharedPreferences by inject()
private var currentJob: Job? = null
override fun onCreate() {
super.onCreate()
Log.d(STATE_TAG, "onCreate()")
}
override fun onBind(intent: Intent?): IBinder? {
Log.d(STATE_TAG, "onBind: intent: $intent")
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY
val asForeground = preferences.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
Log.d(
STATE_TAG,
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
)
if (currentJob != null) {
currentJob?.cancel()
currentJob = null
}
coroutineScope.launch {
currentJob = startPolling().also { it.join() }
}
val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, "long_polling")
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val openCategorySettingsPendingIntent = PendingIntent.getActivity(
this,
1,
openCategorySettingsIntent,
PendingIntent.FLAG_IMMUTABLE
)
if (asForeground) {
val notification =
NotificationsUtils.createNotification(
context = this,
title = "LongPoll",
contentText = "нажмите, чтобы убрать уведомление",
notRemovable = false,
channelId = "long_polling",
priority = NotificationsUtils.NotificationPriority.Low,
category = NotificationCompat.CATEGORY_SERVICE,
customNotificationId = NOTIFICATION_ID,
contentIntent = openCategorySettingsPendingIntent
).build()
startForeground(NOTIFICATION_ID, notification)
} else {
stopForegroundCompat(ServiceCompat.STOP_FOREGROUND_REMOVE)
}
return START_STICKY
}
private fun startPolling(): Job {
if (job.isCompleted || job.isCancelled) {
Log.d(STATE_TAG, "job is completed or cancelled")
throw Exception("Job is over")
}
Log.d(STATE_TAG, "job started")
return coroutineScope.launch {
if (UserConfig.accessToken.isEmpty()) {
throw NoAccessTokenException
}
var serverInfo = getServerInfo()
?: throw LongPollException(message = "bad VK response (server info)")
var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo)
?: throw LongPollException(message = "initiation error: bad VK response (last updates)")
var failCount = 0
while (job.isActive) {
if (lastUpdatesResponse == null) {
failCount++
serverInfo = getServerInfo()
?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)")
lastUpdatesResponse = getUpdatesResponse(serverInfo)
continue
}
when (lastUpdatesResponse.failed) {
1 -> {
val newTs = lastUpdatesResponse.ts ?: kotlin.run {
failCount++
serverInfo.ts
}
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
}
2, 3 -> {
serverInfo = getServerInfo()
?: throw LongPollException(
message = "failed retrieving server info after error: bad VK response (server info #3)"
)
lastUpdatesResponse = getUpdatesResponse(serverInfo)
}
else -> {
val newTs = lastUpdatesResponse.ts
if (newTs == null) {
failCount++
} else {
val updates = lastUpdatesResponse.updates
if (updates == null) {
failCount++
} else {
updates.forEach(updatesParser::parseNextUpdate)
}
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
}
}
}
}
}
}
private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine {
longPollUseCase.getLongPollServer(
needPts = true,
version = VkConstants.LP_VERSION
).listenValue(coroutineScope) { state ->
state.processState(
success = { response ->
Log.d(TAG, "getServerInfo: serverInfoResponse: $response")
it.resume(response)
},
error = { error ->
Log.e(TAG, "getServerInfo: $error")
it.resume(null)
}
)
}
}
private suspend fun getUpdatesResponse(
server: VkLongPollData
): LongPollUpdates? = suspendCoroutine {
longPollUseCase.getLongPollUpdates(
serverUrl = "https://${server.server}",
key = server.key,
ts = server.ts,
wait = 25,
mode = 2 or 8 or 32 or 64 or 128,
version = VkConstants.LP_VERSION
).listenValue(coroutineScope) { state ->
state.processState(
success = { response ->
Log.d(TAG, "lastUpdateResponse: $response")
it.resume(response)
},
error = { error ->
Log.d(TAG, "getUpdatesResponse: error: $error")
it.resume(null)
}
)
}
}
override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy")
try {
SettingsController.edit {
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
}
job.cancel()
} catch (e: Exception) {
e.printStackTrace()
}
super.onDestroy()
}
override fun onLowMemory() {
Log.d(STATE_TAG, "onLowMemory")
super.onLowMemory()
}
companion object {
const val TAG = "LongPollTask"
private const val STATE_TAG = "LongPollServiceState"
const val KEY_LONG_POLL_WAS_DESTROYED = "long_poll_was_destroyed"
private const val NOTIFICATION_ID = 1001
}
}
private data class LongPollException(override val message: String) : Throwable()
private data object NoAccessTokenException : Throwable()
@@ -0,0 +1,13 @@
package com.meloda.app.fast.service.longpolling.di
import com.meloda.app.fast.data.LongPollUpdatesParser
import com.meloda.app.fast.data.LongPollUseCase
import com.meloda.app.fast.data.LongPollUseCaseImpl
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val longPollModule = module {
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
singleOf(::LongPollUpdatesParser)
}
@@ -1,11 +1,11 @@
package com.meloda.fast.util package com.meloda.app.fast.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.meloda.fast.R import com.meloda.app.fast.designsystem.R as UiR
object NotificationsUtils { object NotificationsUtils {
@@ -27,7 +27,7 @@ object NotificationsUtils {
actions: List<NotificationCompat.Action> = emptyList(), actions: List<NotificationCompat.Action> = emptyList(),
): NotificationCompat.Builder { ): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(context, channelId) val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_fast_logo) .setSmallIcon(UiR.drawable.ic_fast_logo)
.setContentTitle(title) .setContentTitle(title)
.setPriority(priority.value) .setPriority(priority.value)
.setContentIntent(contentIntent) .setContentIntent(contentIntent)
@@ -69,5 +69,4 @@ object NotificationsUtils {
enum class NotificationPriority(val value: Int) { enum class NotificationPriority(val value: Int) {
Default(0), Low(-1), Min(-2), High(1), Max(2) Default(0), Low(-1), Min(-2), High(1), Max(2)
} }
} }
@@ -1,26 +0,0 @@
package com.meloda.fast.api
enum class ApiEvent(val value: Int) {
MessageSetFlags(2),
MessageClearFlags(3),
MessageNew(4),
MessageEdit(5),
MessageReadIncoming(6),
MessageReadOutgoing(7),
MessagesDeleted(13),
PinUnpinConversation(20),
PrivateTyping(61),
ChatTyping(62),
OneMoreTyping(63),
VoiceRecording(64),
PhotoUploading(65),
VideoUploading(66),
FileUploading(67),
UnreadCountUpdate(80)
;
companion object {
fun parse(value: Int) = values().firstOrNull { it.value == value }
}
}
@@ -1,7 +0,0 @@
package com.meloda.fast.api
object ApiExtensions {
val Boolean.intString get() = (if (this) 1 else 0).toString()
}
File diff suppressed because it is too large Load Diff
@@ -1,20 +0,0 @@
package com.meloda.fast.api.base
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import okio.IOException
open class ApiError(
@SerializedName("error", alternate = ["error_code"])
val error: String? = null,
@SerializedName("error_msg", alternate = ["error_description"])
open val errorMessage: String? = null,
@SerializedName("error_type")
val errorType: String? = null,
val throwable: Throwable? = null
) : IOException() {
override fun toString(): String {
return Gson().toJson(this)
}
}
@@ -1,8 +0,0 @@
package com.meloda.fast.api.base
data class ApiResponse<T>(
val error: ApiError? = null,
val response: T? = null
) {
val isSuccessful get() = error == null && response != null
}
@@ -1,9 +0,0 @@
package com.meloda.fast.api.base
import com.meloda.fast.api.model.attachments.VkAttachment
import okio.IOException
class AttachmentClassNameIsEmptyException(attachment: VkAttachment) :
IOException(
"attachment ${attachment.javaClass.name} does not have declared field \"className\""
)
@@ -1,34 +0,0 @@
package com.meloda.fast.api.longpoll
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
sealed class LongPollEvent {
data class VkMessageNewEvent(
val message: VkMessage,
val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup>,
) : LongPollEvent()
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
data class VkMessageReadIncomingEvent(
val peerId: Int,
val messageId: Int,
val unreadCount: Int,
) : LongPollEvent()
data class VkMessageReadOutgoingEvent(
val peerId: Int,
val messageId: Int,
val unreadCount: Int,
) : LongPollEvent()
data class VkConversationPinStateChangedEvent(
val peerId: Int,
val majorId: Int,
) : LongPollEvent()
}
@@ -1,300 +0,0 @@
package com.meloda.fast.api.longpoll
import android.util.Log
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.VkUser
import com.meloda.fast.api.network.ApiAnswer
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
import com.meloda.fast.base.viewmodel.VkEventCallback
import com.meloda.fast.data.messages.MessagesRepository
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope {
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d("LongPollUpdatesParser", "error: $throwable")
throwable.printStackTrace()
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job + exceptionHandler
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
mutableMapOf()
fun parseNextUpdate(event: JsonArray) {
val eventId = event[0].asInt
val eventType: ApiEvent? = ApiEvent.parse(eventId)
if (eventType == null) {
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
return
}
when (eventType) {
ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event)
ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event)
ApiEvent.MessageNew -> parseMessageNew(eventType, event)
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event)
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
ApiEvent.VoiceRecording -> onNewEvent(eventType, event)
ApiEvent.PhotoUploading -> onNewEvent(eventType, event)
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 parseConversationPinStateChanged(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt
val majorId = event[2].asInt
launch {
listenersMap[ApiEvent.PinUnpinConversation]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
.onEvent(
LongPollEvent.VkConversationPinStateChangedEvent(
peerId = peerId,
majorId = majorId
)
)
}
}
}
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt
launch {
val newMessageEvent: LongPollEvent.VkMessageNewEvent =
loadNormalMessage(
eventType,
messageId
)
listenersMap[ApiEvent.MessageNew]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
.onEvent(newMessageEvent)
}
}
}
}
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt
launch {
val editedMessageEvent: LongPollEvent.VkMessageEditEvent =
loadNormalMessage(
eventType,
messageId
)
listenersMap[ApiEvent.MessageEdit]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
.onEvent(editedMessageEvent)
}
}
}
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt
val messageId = event[2].asInt
val unreadCount = event[3].asInt
launch {
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
.onEvent(
LongPollEvent.VkMessageReadIncomingEvent(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt
val messageId = event[2].asInt
val unreadCount = event[3].asInt
launch {
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
.onEvent(
LongPollEvent.VkMessageReadOutgoingEvent(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
coroutineScope {
suspendCoroutine {
launch {
val normalMessageResponse = messagesRepository.getById(
MessagesGetByIdRequest(
messagesIds = listOf(messageId),
extended = true,
fields = VKConstants.ALL_FIELDS
)
)
if (normalMessageResponse.isError()) {
normalMessageResponse.error.throwable?.run { throw this }
}
val messagesResponse =
(normalMessageResponse as? ApiAnswer.Success)?.data?.response
?: return@launch
val messagesList = messagesResponse.items
if (messagesList.isEmpty()) return@launch
val normalMessage = messagesList[0].asVkMessage()
messagesRepository.store(listOf(normalMessage))
val profiles = hashMapOf<Int, VkUser>()
messagesResponse.profiles?.forEach { baseUser ->
baseUser.mapToDomain().let { user -> profiles[user.id] = user }
}
val groups = hashMapOf<Int, VkGroup>()
messagesResponse.groups?.forEach { baseGroup ->
baseGroup.mapToDomain().let { group -> groups[group.id] = group }
}
val resumeValue: LongPollEvent? = when (eventType) {
ApiEvent.MessageNew ->
LongPollEvent.VkMessageNewEvent(
normalMessage,
profiles,
groups
)
ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage)
else -> null
}
resumeValue?.let { value -> it.resume(value as T) }
}
}
}
private fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
listenersMap.let { map ->
map[eventType] = (map[eventType] ?: mutableListOf()).also {
it.add(listener)
}
}
}
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
registerListener(ApiEvent.PinUnpinConversation, listener)
}
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
onConversationPinStateChanged(assembleEventCallback(block))
}
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
registerListener(ApiEvent.MessageReadIncoming, listener)
}
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
onMessageIncomingRead(assembleEventCallback(block))
}
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
registerListener(ApiEvent.MessageReadOutgoing, listener)
}
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
onMessageOutgoingRead(assembleEventCallback(block))
}
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
registerListener(ApiEvent.MessageNew, listener)
}
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
onNewMessage(assembleEventCallback(block))
}
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
registerListener(ApiEvent.MessageEdit, listener)
}
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
onMessageEdited(assembleEventCallback(block))
}
fun clearListeners() {
listenersMap.clear()
}
}
internal inline fun <R : Any> assembleEventCallback(
crossinline block: (R) -> Unit,
): VkEventCallback<R> {
return VkEventCallback { event -> block.invoke(event) }
}
@@ -1,17 +0,0 @@
package com.meloda.fast.api.model
sealed class ActionState {
object Phantom : ActionState()
object CallInProgress : ActionState()
object None : ActionState()
companion object {
fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState {
return when {
isPhantom -> Phantom
isCallInProgress -> CallInProgress
else -> None
}
}
}
}
@@ -1,25 +0,0 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class ConversationPeerType : Parcelable {
object User : ConversationPeerType()
object Group : ConversationPeerType()
object Chat : ConversationPeerType()
fun isUser() = this == User
fun isGroup() = this == Group
fun isChat() = this == Chat
companion object {
fun parse(type: String): ConversationPeerType {
return when (type) {
"user" -> User
"group" -> Group
else -> Chat
}
}
}
}
@@ -1,14 +0,0 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkChatMember(
val memberId: Int,
val invitedBy: Int,
val joinDate: Int,
val isAdmin: Boolean,
val isOwner: Boolean,
val canKick: Boolean
) : Parcelable
@@ -1,21 +0,0 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Entity(tableName = "groups")
@Parcelize
data class VkGroup(
@PrimaryKey(autoGenerate = false)
val id: Int,
val name: String,
val screenName: String,
val photo200: String?,
val membersCount: Int?
) : Parcelable {
override fun toString() = name.trim()
}
@@ -1,132 +0,0 @@
package com.meloda.fast.api.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.domain.VkConversationDomain
import com.meloda.fast.model.SelectableItem
import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database
@Entity(tableName = "messages")
@Parcelize
data class VkMessage constructor(
@PrimaryKey(autoGenerate = false)
var id: Int,
var text: String? = null,
val isOut: Boolean,
val peerId: Int,
val fromId: Int,
val date: Int,
val randomId: Int,
val action: String? = null,
val actionMemberId: Int? = null,
val actionText: String? = null,
val actionConversationMessageId: Int? = null,
val actionMessage: String? = null,
var updateTime: Int? = null,
var important: Boolean = false,
var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null,
var replyMessage: VkMessage? = null,
val geo: BaseVkMessage.Geo? = null,
) : SelectableItem() {
@Ignore
@IgnoredOnParcel
var user: VkUser? = null
@Ignore
@IgnoredOnParcel
var group: VkGroup? = null
@Ignore
@IgnoredOnParcel
var actionUser: VkUser? = null
@Ignore
@IgnoredOnParcel
var actionGroup: VkGroup? = null
@Ignore
@IgnoredOnParcel
var state: State = State.Sent
fun isPeerChat() = peerId > 2_000_000_000
fun isUser() = fromId > 0
fun isGroup() = fromId < 0
fun isRead(conversation: VkConversationDomain) =
if (isOut) {
conversation.outRead - id >= 0
} else {
conversation.inRead - id >= 0
}
fun getPreparedAction(): Action? {
if (action == null) return null
return Action.parse(action)
}
fun canEdit() =
fromId == UserConfig.userId &&
(attachments == null ||
!VKConstants.restrictedToEditAttachments.contains(
requireNotNull(attachments).first().javaClass
)) &&
(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) {
CHAT_CREATE("chat_create"),
CHAT_PHOTO_UPDATE("chat_photo_update"),
CHAT_PHOTO_REMOVE("chat_photo_remove"),
CHAT_TITLE_UPDATE("chat_title_update"),
CHAT_PIN_MESSAGE("chat_pin_message"),
CHAT_UNPIN_MESSAGE("chat_unpin_message"),
CHAT_INVITE_USER("chat_invite_user"),
CHAT_INVITE_USER_BY_LINK("chat_invite_user_by_link"),
CHAT_KICK_USER("chat_kick_user"),
CHAT_SCREENSHOT("chat_screenshot"),
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
CHAT_STYLE_UPDATE("conversation_style_update");
companion object {
fun parse(value: String?): Action? = values().firstOrNull { it.value == value }
}
}
enum class State {
Sending, Sent, Error
}
}
@@ -1,26 +0,0 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Entity(tableName = "users")
@Parcelize
data class VkUser(
@PrimaryKey(autoGenerate = false)
val id: Int,
val firstName: String,
val lastName: String,
val online: Boolean,
val photo200: String?,
val lastSeen: Int?,
val lastSeenStatus: String?,
val birthday: String?
) : Parcelable {
override fun toString() = fullName
val fullName get() = "$firstName $lastName".trim()
}
@@ -1,12 +0,0 @@
package com.meloda.fast.api.model.attachments
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
open class VkAttachment : Parcelable {
open fun asString(withAccessKey: Boolean = true) = ""
}
@@ -1,28 +0,0 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.VkUtils
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkAudio(
val id: Int,
val ownerId: Int,
val title: String,
val artist: String,
val url: String,
val duration: Int,
val accessKey: String?
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
attachmentClass = this::class.java,
id = id,
ownerId = ownerId,
withAccessKey = withAccessKey,
accessKey = accessKey
)
}
@@ -1,19 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkCall(
val initiatorId: Int,
val receiverId: Int,
val state: String,
val time: Int,
val duration: Int,
val isVideo: Boolean
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkCurator(
val id: Int,
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkEvent(
val id: Int
) : VkAttachment()
@@ -1,30 +0,0 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.base.attachments.BaseVkFile
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkFile(
val id: Int,
val ownerId: Int,
val title: String,
val ext: String,
val size: Int,
val url: String,
val accessKey: String?,
val preview: BaseVkFile.Preview?
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
attachmentClass = this::class.java,
id = id,
ownerId = ownerId,
withAccessKey = withAccessKey,
accessKey = accessKey
)
}
@@ -1,16 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGift(
val id: Int,
val thumb256: String?,
val thumb96: String?,
val thumb48: String
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,18 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGraffiti(
val id: Int,
val ownerId: Int,
val url: String,
val width: Int,
val height: Int,
val accessKey: String
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGroupCall(
val initiatorId: Int
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,18 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkLink(
val url: String,
val title: String?,
val caption: String?,
val photo: VkPhoto?,
val target: String?,
val isFavorite: Boolean
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkMiniApp(
val link: String
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkPoll(
val id: Int
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,26 +0,0 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkSticker(
val id: Int,
val productId: Int,
val images: List<BaseVkSticker.Image>,
val backgroundImages: List<BaseVkSticker.Image>
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun urlForSize(size: Int): String? {
for (image in images) {
if (image.width == size) return image.url
}
return null
}
}
@@ -1,21 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkStory(
val id: Int,
val ownerId: Int,
val date: Int,
val photo: VkPhoto?
) : VkAttachment() {
fun isFromUser() = ownerId > 0
fun isFromGroup() = ownerId < 0
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,22 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkVoiceMessage(
val id: Int,
val ownerId: Int,
val duration: Int,
val waveform: List<Int>,
val linkOgg: String,
val linkMp3: String,
val accessKey: String,
val transcriptState: String?,
val transcript: String?
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,25 +0,0 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWall(
val id: Int,
val fromId: Int,
val toId: Int,
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
val comments: Int?,
val likes: Int?,
val reposts: Int?,
val views: Int?,
val isFavorite: Boolean,
val accessKey: String?
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWallReply(
val id: Int
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,13 +0,0 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWidget(
val id: Int
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,38 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.meloda.fast.api.model.VkChat
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkChat(
val type: String,
val title: String,
val admin_id: Int,
val members_count: Int,
val id: Int,
val photo_50: String,
val photo_100: String,
val photo_200: String,
val is_default_photo: Boolean,
val push_settings: PushSettings
) : Parcelable {
fun asVkChat() = VkChat(
type = type,
title = title,
adminId = admin_id,
membersCount = members_count,
id = id,
photo50 = photo_50,
photo100 = photo_100,
photo200 = photo_200,
isDefaultPhoto = is_default_photo
)
@Parcelize
data class PushSettings(
val sound: Int,
val disabled_until: Int
) : Parcelable
}
@@ -1,26 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.meloda.fast.api.model.VkChatMember
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkChatMember(
val member_id: Int,
val invited_by: Int,
val join_date: Int,
val is_admin: Boolean?,
val is_owner: Boolean?,
val can_kick: Boolean?
) : Parcelable {
fun asVkChatMember() = VkChatMember(
memberId = member_id,
invitedBy = invited_by,
joinDate = join_date,
isAdmin = is_admin == true,
isOwner = is_owner == true,
canKick = can_kick == true
)
}
@@ -1,31 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.meloda.fast.api.model.VkGroup
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGroup(
val id: Int,
val name: String,
val screen_name: String,
val is_closed: Int,
val type: String,
val is_admin: Int,
val is_member: Int,
val is_advertiser: Int,
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val members_count: Int?
) : Parcelable {
fun mapToDomain() = VkGroup(
id = -id,
name = name,
screenName = screen_name,
photo200 = photo_200,
membersCount = members_count
)
}
@@ -1,12 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkLongPoll(
val server: String,
val key: String,
val ts: Int,
val pts: Int
) : Parcelable
@@ -1,76 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkMessage(
val id: Int,
val peer_id: Int,
val date: Int,
val from_id: Int,
val out: Int,
val text: String,
val conversation_message_id: Int,
val fwd_messages: List<BaseVkMessage>? = emptyList(),
val important: Boolean,
val random_id: Int,
val attachments: List<BaseVkAttachmentItem> = emptyList(),
val is_hidden: Boolean,
val payload: String,
val geo: Geo?,
val action: Action?,
val ttl: Int,
val reply_message: BaseVkMessage?,
val update_time: Int?
) : Parcelable {
fun asVkMessage() = VkMessage(
id = id,
text = text.ifBlank { null },
isOut = out == 1,
peerId = peer_id,
fromId = from_id,
date = date,
randomId = random_id,
action = action?.type,
actionMemberId = action?.member_id,
actionText = action?.text,
actionConversationMessageId = action?.conversation_message_id,
actionMessage = action?.message,
geo = geo,
important = important,
updateTime = update_time
).also {
it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwd_messages)
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
}
@Parcelize
data class Geo(
val type: String,
val coordinates: Coordinates,
val place: Place
) : Parcelable {
@Parcelize
data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable
@Parcelize
data class Place(val country: String, val city: String, val title: String) : Parcelable
}
@Parcelize
data class Action(
val type: String,
val member_id: Int?,
val text: String?,
val conversation_message_id: Int?,
val message: String?
) : Parcelable
}
@@ -1,47 +0,0 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.meloda.fast.api.model.VkUser
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkUser(
val id: Int,
val first_name: String,
val last_name: String,
val can_access_closed: Boolean,
val is_closed: Boolean,
val can_invite_to_chats: Boolean,
val sex: Int?,
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val online: Int?,
val online_info: OnlineInfo?,
val screen_name: String,
val bdate: String?
//...other fields
) : Parcelable {
@Parcelize
data class OnlineInfo(
val visible: Boolean,
val status: String,
val last_seen: Int?,
val is_online: Boolean?,
val online_mobile: Boolean?,
val app_id: Int?
) : Parcelable
fun mapToDomain() = VkUser(
id = id,
firstName = first_name,
lastName = last_name,
online = online == 1,
photo200 = photo_200,
lastSeen = online_info?.last_seen,
lastSeenStatus = online_info?.status,
birthday = bdate
)
}
@@ -1,77 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import android.util.Log
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkAttachmentItem(
val type: String,
val photo: BaseVkPhoto?,
val video: BaseVkVideo?,
val audio: BaseVkAudio?,
@SerializedName("doc")
val file: BaseVkFile?,
val link: BaseVkLink?,
@SerializedName("mini_app")
val miniApp: BaseVkMiniApp?,
@SerializedName("audio_message")
val voiceMessage: BaseVkVoiceMessage?,
val sticker: BaseVkSticker?,
val gift: BaseVkGift?,
val wall: BaseVkWall?,
val graffiti: BaseVkGraffiti?,
val poll: BaseVkPoll?,
@SerializedName("wall_reply")
val wallReply: BaseVkWallReply?,
val call: BaseVkCall?,
@SerializedName("group_call_in_progress")
val groupCall: BaseVkGroupCall?,
val curator: BaseVkCurator?,
val event: BaseVkEvent?,
val story: BaseVkStory?,
val widget: BaseVkWidget?
) : Parcelable {
fun getPreparedType() = AttachmentType.parse(type)
enum class AttachmentType(var value: String) {
Unknown("unknown"),
Photo("photo"),
Video("video"),
Audio("audio"),
File("doc"),
Link("link"),
Voice("audio_message"),
MiniApp("mini_app"),
Sticker("sticker"),
Gift("gift"),
Wall("wall"),
Graffiti("graffiti"),
Poll("poll"),
WallReply("wall_reply"),
Call("call"),
GroupCallInProgress("group_call_in_progress"),
Curator("curator"),
Event("event"),
Story("story"),
Widget("widget")
;
companion object {
fun parse(value: String): AttachmentType {
val parsedValue = values().firstOrNull { it.value == value } ?: Unknown
if (parsedValue == Unknown) {
Log.e("AttachmentType", "Unknown attachment type: $value")
}
return parsedValue
}
}
}
}
abstract class BaseVkAttachment : Parcelable
@@ -1,60 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkAudio
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkAudio(
val id: Int,
val title: String,
val artist: String,
val duration: Int,
val url: String,
val date: Int,
val owner_id: Int,
val access_key: String?,
val is_explicit: Boolean,
val is_focus_track: Boolean,
val is_licensed: Boolean,
val track_code: String,
val genre_id: Int,
val album: Album,
val short_videos_allowed: Boolean,
val stories_allowed: Boolean,
val stories_cover_allowed: Boolean
) : BaseVkAttachment() {
fun asVkAudio() = VkAudio(
id = id,
ownerId = owner_id,
title = title,
artist = artist,
url = url,
duration = duration,
accessKey = access_key
)
@Parcelize
data class Album(
val id: Int,
val title: String,
val owner_id: Int,
val access_key: String,
val thumb: Thumb
) : Parcelable {
@Parcelize
data class Thumb(
val width: Int,
val height: Int,
val photo_34: String,
val photo_68: String,
val photo_135: String,
val photo_270: String,
val photo_300: String,
val photo_600: String,
val photo_1200: String
) : Parcelable
}
}
@@ -1,26 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkCall
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkCall(
val initiator_id: Int,
val receiver_id: Int,
val state: String,
val time: Int,
val duration: Int,
val video: Boolean
) : Parcelable {
fun asVkCall() = VkCall(
initiatorId = initiator_id,
receiverId = receiver_id,
state = state,
time = time,
duration = duration,
isVideo = video
)
}
@@ -1,27 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkCurator
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkCurator(
val id: Int,
val name: String,
val description: String,
val url: String,
val photo: List<Photo>
) : BaseVkAttachment() {
fun asVkCurator() = VkCurator(
id = id
)
@Parcelize
data class Photo(
val height: Int,
val url: String,
val width: String
) : Parcelable
}
@@ -1,20 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import com.meloda.fast.api.model.attachments.VkEvent
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkEvent(
val button_text: String,
val id: Int,
val is_favorite: Boolean,
val text: String,
val address: String,
val friends: List<Int> = emptyList(),
val member_status: Int,
val time: Int
) : BaseVkAttachment() {
fun asVkEvent() = VkEvent(id = id)
}
@@ -1,63 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkFile
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkFile(
val id: Int,
val owner_id: Int,
val title: String,
val size: Int,
val ext: String,
val date: Int,
val type: Int,
val url: String,
val preview: Preview?,
val ic_licensed: Int,
val access_key: String?,
val web_preview_url: String?
) : BaseVkAttachment() {
fun asVkFile() = VkFile(
id = id,
ownerId = owner_id,
title = title,
ext = ext,
url = url,
size = size,
accessKey = access_key,
preview = preview
)
@Parcelize
data class Preview(
val photo: Photo?,
val video: Video?
) : Parcelable {
@Parcelize
data class Photo(val sizes: List<Size>) : Parcelable {
@Parcelize
data class Size(
val height: Int,
val width: Int,
val type: String,
val src: String
) : Parcelable
}
@Parcelize
data class Video(
val src: String,
val width: Int,
val height: Int,
val file_size: Int
) : Parcelable
}
}
@@ -1,22 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkGift
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGift(
val id: Int,
val thumb_256: String?,
val thumb_96: String?,
val thumb_48: String
) : Parcelable {
fun asVkGift() = VkGift(
id = id,
thumb256 = thumb_256,
thumb96 = thumb_96,
thumb48 = thumb_48
)
}
@@ -1,26 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkGraffiti
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGraffiti(
val id: Int,
val owner_id: Int,
val url: String,
val width: Int,
val height: Int,
val access_key: String
) : Parcelable {
fun asVkGraffiti() = VkGraffiti(
id = id,
ownerId = owner_id,
url = url,
width = width,
height = height,
accessKey = access_key
)
}
@@ -1,22 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkGroupCall
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGroupCall(
val initiator_id: Int,
val join_link: String,
val participants: Participants
) : Parcelable {
@Parcelize
data class Participants(
val list: List<Int>,
val count: Int
) : Parcelable
fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id)
}
@@ -1,25 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import com.meloda.fast.api.model.attachments.VkLink
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkLink(
val url: String,
val title: String?,
val caption: String?,
val photo: BaseVkPhoto?,
val target: String?,
val is_favorite: Boolean
) : BaseVkAttachment() {
fun asVkLink() = VkLink(
url = url,
title = title,
caption = caption,
photo = photo?.asVkPhoto(),
target = target,
isFavorite = is_favorite
)
}
@@ -1,69 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkMiniApp
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkMiniApp(
val title: String,
val description: String,
val app: App,
val images: List<Image>?,
val button_text: String
) : Parcelable {
@Parcelize
data class App(
val type: String,
val id: Int,
val title: String,
@SerializedName("author_owner_id")
val authorOwnerId: Int,
@SerializedName("are_notifications_enabled")
val areNotificationsEnabled: Boolean,
@SerializedName("is_favorite")
val isFavorite: Boolean,
@SerializedName("is_installed")
val isInstalled: Boolean,
@SerializedName("track_code")
val trackCode: String,
@SerializedName("share_url")
val shareUrl: String,
@SerializedName("webview_url")
val webViewUrl: String,
@SerializedName("hide_tabbar")
val hideTabBar: Int,
@SerializedName("icon_75")
val icon75: String?,
@SerializedName("icon_139")
val icon139: String?,
@SerializedName("icon_150")
val icon150: String?,
@SerializedName("icon_278")
val icon278: String?,
@SerializedName("icon_576")
val icon576: String?,
@SerializedName("open_in_external_browser")
val openInExternalBrowser: Boolean,
@SerializedName("need_policy_confirmation")
val needPolicyConfirmation: Boolean,
@SerializedName("is_vkui_internal")
val isVkUiInternal: Boolean,
@SerializedName("has_vk_connect")
val hasVkConnect: Boolean,
@SerializedName("need_show_bottom_menu_tooltip_on_close")
val needShowBottomMenuTooltipOnClose: Boolean
) : Parcelable
@Parcelize
data class Image(
val height: Int,
val width: Int,
val url: String
) : Parcelable
fun asVkMiniApp() = VkMiniApp(link = app.shareUrl)
}
@@ -1,43 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkPhoto
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkPhoto(
val album_id: Int,
val date: Int,
val id: Int,
val owner_id: Int,
val has_tags: Boolean,
val access_key: String?,
val sizes: List<Size>,
val text: String?,
val user_id: Int?,
val lat: Double?,
val long: Double?,
val post_id: Int?
) : BaseVkAttachment() {
fun asVkPhoto() = VkPhoto(
albumId = album_id,
date = date,
id = id,
ownerId = owner_id,
hasTags = has_tags,
accessKey = access_key,
sizes = sizes,
text = text,
userId = user_id
)
@Parcelize
data class Size(
val height: Int,
val width: Int,
val type: String,
val url: String
) : Parcelable
}
@@ -1,63 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkPoll
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkPoll(
val multiple: Boolean,
val id: Int,
val votes: Int,
val anonymous: Boolean,
val closed: Boolean,
val end_date: Int,
val is_board: Boolean,
val can_vote: Boolean,
val can_edit: Boolean,
val can_report: Boolean,
val can_share: Boolean,
val created: Int,
val owner_id: Int,
val question: String,
val disable_unvote: Boolean,
val friends: List<Friend>?,
val embed_hash: String,
val answers: List<Answer>,
val author_id: Int,
val background: Background?
) : Parcelable {
@Parcelize
data class Friend(
val id: Int
) : Parcelable
@Parcelize
data class Answer(
val id: Int,
val rate: Double,
val text: String,
val votes: Int
) : Parcelable
@Parcelize
data class Background(
val angle: Int,
val color: String,
val id: Int,
val name: String,
val type: String,
val points: List<Point>
) : Parcelable {
@Parcelize
data class Point(
val color: String,
val position: Double
) : Parcelable
}
fun asVkPoll() = VkPoll(id = id)
}
@@ -1,37 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkSticker
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkSticker(
val product_id: Int,
val sticker_id: Int,
val images: List<Image>,
val images_with_background: List<Image>,
val animation_url: String?,
val animations: List<Animation>?
) : Parcelable {
fun asVkSticker() = VkSticker(
id = sticker_id,
productId = product_id,
images = images,
backgroundImages = images_with_background
)
@Parcelize
data class Image(
val width: Int,
val height: Int,
val url: String
) : Parcelable
@Parcelize
data class Animation(
val type: String,
val url: String
) : Parcelable
}
@@ -1,52 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkStory
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkStory(
val id: Int,
val owner_id: Int,
val access_key: String,
val can_comment: Int,
val can_reply: Int,
val can_like: Boolean,
val can_share: Int,
val can_hide: Int,
val date: Int,
val expires_at: Int,
val is_ads: Boolean,
val photo: BaseVkPhoto?,
val replies: Replies,
val is_one_time: Boolean,
val track_code: String,
val type: String,
val views: Int,
val likes_count: Int,
val reaction_set_id: String,
val is_restricted: Boolean,
val no_sound: Boolean,
val need_mute: Boolean,
val mute_reply: Boolean,
val can_ask: Int,
val can_ask_anonymous: Int,
val preloading_enabled: Boolean,
val narratives_count: Int,
val can_use_in_narrative: Boolean
) : BaseVkAttachment() {
fun asVkStory() = VkStory(
id = id,
ownerId = owner_id,
date = date,
photo = photo?.asVkPhoto()
)
@Parcelize
data class Replies(
val count: Int,
val new: Int
) : Parcelable
}
@@ -1,140 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkVideo
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkVideo(
val id: Int,
val title: String,
val width: Int,
val height: Int,
val duration: Int,
val date: Int,
val comments: Int,
val description: String,
val player: String,
val added: Int,
val type: String,
val views: Int,
val can_comment: Int,
val can_edit: Int,
val can_like: Int,
val can_repost: Int,
val can_subscribe: Int,
val can_add_to_faves: Int,
val can_add: Int,
val can_attach_link: Int,
val access_key: String?,
val owner_id: Int,
val ov_id: String,
val is_favorite: Boolean,
val track_code: String,
val image: List<Image>,
val first_frame: List<FirstFrame>,
val files: File,
val timeline_thumbs: TimelineThumbs,
val ads: Ads
) : BaseVkAttachment() {
fun asVkVideo() = VkVideo(
id = id,
ownerId = owner_id,
images = image.map { it.asVideoImage() },
firstFrames = first_frame,
accessKey = access_key,
title = title
)
@Parcelize
data class Image(
val width: Int,
val height: Int,
val url: String,
val with_padding: Int?
) : Parcelable {
fun asVideoImage() = VkVideo.VideoImage(
width = width,
height = height,
url = url,
withPadding = with_padding == 1
)
}
@Parcelize
data class FirstFrame(
val height: Int,
val width: Int,
val url: String
) : Parcelable
@Parcelize
data class File(
val mp4_240: String?,
val mp4_360: String?,
val mp4_480: String?,
val mp4_720: String?,
val mp4_1080: String?,
val mp4_1440: String?,
val hls: String,
val dash_uni: String,
val dash_sep: String,
val hls_ondemand: String,
val dash_ondemand: String,
val failover_host: String
) : Parcelable
@Parcelize
data class TimelineThumbs(
val count_per_image: Int,
val count_per_row: Int,
val count_total: Int,
val frame_height: Int,
val frame_width: Float,
val links: List<String>,
val is_uv: Boolean,
val frequency: Int
) : Parcelable
@Parcelize
data class Ads(
val slot_id: Int,
val timeout: Int,
val can_play: Int,
val params: Params,
val sections: List<String>,
val midroll_percents: List<Float>
) : Parcelable {
@Parcelize
data class Params(
val vk_id: Int,
val duration: Int,
val video_id: String,
val pl: Int,
val content_id: String,
val lang: Int,
val puid1: String,
val puid2: Int,
val puid3: Int,
val puid5: Int,
val puid6: Int,
val puid7: Int,
val puid9: Int,
val puid10: Int,
val puid12: Int,
val puid13: Int,
val puid14: Int,
val puid15: Int,
val puid18: Int,
val puid21: Int,
val sign: String,
val groupId: Int,
val vk_catid: Int,
val is_xz_video: Int
) : Parcelable
}
}
@@ -1,32 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkVoiceMessage
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkVoiceMessage(
val id: Int,
val owner_id: Int,
val duration: Int,
val waveform: List<Int>,
val link_ogg: String,
val link_mp3: String,
val access_key: String,
val transcript_state: String?,
val transcript: String?
) : Parcelable {
fun asVkVoiceMessage() = VkVoiceMessage(
id = id,
ownerId = owner_id,
duration = duration,
waveform = waveform,
linkOgg = link_ogg,
linkMp3 = link_mp3,
accessKey = access_key,
transcriptState = transcript_state,
transcript = transcript
)
}
@@ -1,78 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkWall
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkWall(
val id: Int,
val from_id: Int,
val to_id: Int,
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
val post_source: PostSource?,
val comments: Comments?,
val likes: Likes?,
val reposts: Reposts?,
val views: Views?,
val is_favorite: Boolean,
val donut: Donut?,
val access_key: String?,
val short_text_rate: Double
) : Parcelable {
fun asVkWall() = VkWall(
id = id,
fromId = from_id,
toId = to_id,
date = date,
text = text,
attachments = attachments,
comments = comments?.count,
likes = likes?.count,
reposts = reposts?.count,
views = views?.count,
isFavorite = is_favorite,
accessKey = access_key
)
@Parcelize
data class PostSource(
val type: String,
val platform: String
) : Parcelable
@Parcelize
data class Comments(
val count: Int,
val can_post: Int,
val groups_can_post: Boolean
) : Parcelable
@Parcelize
data class Likes(
val count: Int,
val user_likes: Int,
val can_like: Int,
val can_publish: Int,
) : Parcelable
@Parcelize
data class Reposts(
val count: Int,
val user_reposted: Int
) : Parcelable
@Parcelize
data class Views(
val count: Int
) : Parcelable
@Parcelize
data class Donut(
val is_donut: Boolean
) : Parcelable
}
@@ -1,11 +0,0 @@
package com.meloda.fast.api.model.base.attachments
import com.meloda.fast.api.model.attachments.VkWidget
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkWidget(val id: Int) : BaseVkAttachment() {
fun asVkWidget() = VkWidget(id)
}
@@ -1,147 +0,0 @@
package com.meloda.fast.api.model.data
import android.os.Parcelable
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
import com.meloda.fast.api.model.domain.VkConversationDomain
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkConversation(
val peer: Peer,
val last_message_id: Int,
val in_read: Int,
val out_read: Int,
val in_read_cmid: Int,
val out_read_cmid: Int,
val sort_id: SortId,
val last_conversation_message_id: Int,
val is_marked_unread: Boolean,
val important: Boolean,
val push_settings: PushSettings,
val can_write: CanWrite,
val can_send_money: Boolean,
val can_receive_money: Boolean,
val chat_settings: ChatSettings?,
val call_in_progress: CallInProgress?,
val unread_count: Int?,
) : Parcelable {
@Parcelize
data class Peer(
val id: Int,
val type: String,
val local_id: Int,
) : Parcelable
@Parcelize
data class SortId(
val major_id: Int,
val minor_id: Int,
) : Parcelable
@Parcelize
data class PushSettings(
val disabled_forever: Boolean,
val no_sound: Boolean,
val disabled_mentions: Boolean,
val disabled_mass_mentions: Boolean,
) : Parcelable
@Parcelize
data class CanWrite(
val allowed: Boolean,
) : Parcelable
@Parcelize
data class ChatSettings(
val owner_id: Int,
val title: String,
val state: String,
val acl: Acl,
val members_count: Int,
val friends_count: Int,
val photo: Photo?,
val admin_ids: List<Int>,
val active_ids: List<Int>,
val is_group_channel: Boolean,
val is_disappearing: Boolean,
val is_service: Boolean,
val theme: String?,
val pinned_message: BaseVkMessage?,
) : Parcelable {
@Parcelize
data class Acl(
val can_change_info: Boolean,
val can_change_invite_link: Boolean,
val can_change_pin: Boolean,
val can_invite: Boolean,
val can_promote_users: Boolean,
val can_see_invite_link: Boolean,
val can_moderate: Boolean,
val can_copy_chat: Boolean,
val can_call: Boolean,
val can_use_mass_mentions: Boolean,
val can_change_style: Boolean,
) : Parcelable
@Parcelize
data class Photo(
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val is_default_photo: Boolean,
) : Parcelable
}
@Parcelize
data class CallInProgress(
val participants: BaseVkGroupCall.Participants,
val join_link: String,
) : Parcelable {
@Parcelize
data class Participants(
val list: List<Int>,
val count: Int,
) : Parcelable
}
fun mapToDomain(
lastMessage: VkMessage? = null,
conversationUser: VkUser? = null,
conversationGroup: VkGroup? = null,
) = VkConversationDomain(
id = peer.id,
localId = peer.local_id,
conversationTitle = chat_settings?.title,
conversationPhoto = chat_settings?.photo?.photo_200,
type = peer.type,
isCallInProgress = call_in_progress != null,
isPhantom = chat_settings?.is_disappearing == true,
lastConversationMessageId = last_conversation_message_id,
inRead = in_read,
outRead = out_read,
lastMessageId = last_message_id,
unreadCount = unread_count ?: 0,
membersCount = chat_settings?.members_count,
ownerId = chat_settings?.owner_id,
majorId = sort_id.major_id,
minorId = sort_id.minor_id,
canChangePin = chat_settings?.acl?.can_change_pin == true,
canChangeInfo = chat_settings?.acl?.can_change_info == true,
pinnedMessageId = chat_settings?.pinned_message?.id,
inReadCmId = in_read_cmid,
outReadCmId = out_read_cmid,
).also {
it.lastMessage = lastMessage
it.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
it.conversationUser = conversationUser
it.conversationGroup = conversationGroup
}
}
@@ -1,245 +0,0 @@
package com.meloda.fast.api.model.domain
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.ActionState
import com.meloda.fast.api.model.ConversationPeerType
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.presentation.VkConversationUi
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.ext.isFalse
import com.meloda.fast.ext.isTrue
import com.meloda.fast.ext.orDots
import com.meloda.fast.model.base.UiImage
import com.meloda.fast.model.base.UiText
import com.meloda.fast.model.base.parseString
import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.Calendar
@Suppress("MemberVisibilityCanBePrivate")
@Entity(tableName = "conversations")
@Parcelize
data class VkConversationDomain(
@PrimaryKey(autoGenerate = false)
val id: Int,
val localId: Int,
val ownerId: Int?,
val conversationTitle: String?,
val conversationPhoto: String?,
val isCallInProgress: Boolean,
val isPhantom: Boolean,
val lastConversationMessageId: Int,
val inReadCmId: Int,
val outReadCmId: Int,
val inRead: Int,
val outRead: Int,
val lastMessageId: Int,
val unreadCount: Int,
val membersCount: Int?,
val canChangePin: Boolean,
val canChangeInfo: Boolean,
val majorId: Int,
val minorId: Int,
val pinnedMessageId: Int?,
val type: String,
) : Parcelable {
@Ignore
@IgnoredOnParcel
var peerType: ConversationPeerType = ConversationPeerType.parse(type)
@Ignore
@IgnoredOnParcel
var lastMessage: VkMessage? = null
@Ignore
@IgnoredOnParcel
var pinnedMessage: VkMessage? = null
@Ignore
@IgnoredOnParcel
var conversationUser: VkUser? = null
@Ignore
@IgnoredOnParcel
var conversationGroup: VkGroup? = null
fun isChat() = peerType.isChat()
fun isUser() = peerType.isUser()
fun isGroup() = peerType.isGroup()
fun isInUnread() = inRead - lastMessageId < 0
fun isOutUnread() = outRead - lastMessageId < 0
fun isUnread() = isInUnread() || isOutUnread()
fun isAccount() = id == UserConfig.userId
fun isPinned() = majorId > 0
fun extractAvatar(): UiImage {
val placeholderImage = UiImage.Resource(R.drawable.ic_account_circle_cut)
val avatarLink = when {
peerType.isUser() -> {
if (id == UserConfig.userId) {
null
} else {
conversationUser?.photo200
}
}
peerType.isGroup() -> conversationGroup?.photo200
peerType.isChat() -> conversationPhoto
else -> null
}
return avatarLink?.let(UiImage::Url) ?: placeholderImage
}
fun extractTitle(): UiText {
return when {
isAccount() -> UiText.Resource(R.string.favorites)
peerType.isChat() -> UiText.Simple(conversationTitle ?: "...")
peerType.isUser() -> UiText.Simple(conversationUser?.fullName ?: "...")
peerType.isGroup() -> UiText.Simple(conversationGroup?.name ?: "...")
else -> UiText.Simple("...")
}
}
fun extractUnreadCounterText(): String? {
if (lastMessage?.isOut.isFalse && !isInUnread()) return null
return when (unreadCount) {
in 1..999 -> unreadCount.toString()
0 -> null
else -> "%dK".format(unreadCount / 1000)
}
}
// TODO: 07.01.2023, Danil Nikolaev: rewrite
fun extractMessage(): String {
val actionMessage = VkUtils.getActionConversationText(
message = lastMessage,
youPrefix = "You",
messageUser = lastMessage?.user,
messageGroup = lastMessage?.group,
action = lastMessage?.getPreparedAction(),
actionUser = lastMessage?.actionUser,
actionGroup = lastMessage?.actionGroup
)
val attachmentIcon: UiImage? = when {
lastMessage?.text == null -> null
!lastMessage?.forwards.isNullOrEmpty() -> {
if (lastMessage?.forwards?.size == 1) {
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
} else {
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
}
}
else -> VkUtils.getAttachmentConversationIcon(lastMessage)
}
val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText(
message = lastMessage
) else null)
val forwardsMessage = (if (lastMessage?.text == null) VkUtils.getForwardsText(
message = lastMessage
) else null)
val messageText = lastMessage?.text?.let(UiText::Simple)
var prefix = when {
actionMessage != null -> ""
lastMessage?.isOut.isTrue -> "You: "
else ->
when {
lastMessage?.user != null && lastMessage?.user?.firstName?.isNotBlank().isTrue -> {
"${lastMessage?.user?.firstName}: "
}
lastMessage?.group != null && lastMessage?.group?.name?.isNotBlank().isTrue -> {
"${lastMessage?.group?.name}: "
}
else -> ""
}
}
if ((!peerType.isChat() && lastMessage?.isOut.isFalse) || id == UserConfig.userId)
prefix = ""
val finalText =
(actionMessage ?: forwardsMessage ?: attachmentText ?: messageText)
?.parseString(AppGlobal.Instance)
?.let(VkUtils::prepareMessageText)
?.let { text -> "$prefix$text" }
return finalText.orDots()
}
fun extractAttachmentImage(): UiImage? {
if (lastMessage?.text == null) return null
return VkUtils.getAttachmentConversationIcon(lastMessage)
}
fun extractReadCondition(): Boolean {
return (lastMessage?.isOut.isTrue && isOutUnread()) ||
(lastMessage?.isOut.isFalse && isInUnread())
}
fun extractDate(): String {
return TimeUtils.getLocalizedTime(AppGlobal.Instance, (lastMessage?.date ?: -1) * 1000L)
}
// TODO: 05.08.2023, Danil Nikolaev: rewrite
fun extractBirthday(): Boolean {
val birthday = conversationUser?.birthday ?: return false
val splitBirthday = birthday.split(".")
return if (splitBirthday.size > 1) {
val birthdayCalendar = Calendar.getInstance().apply {
this[Calendar.DAY_OF_MONTH] = splitBirthday.first().toIntOrNull() ?: -1
this[Calendar.MONTH] = (splitBirthday[1].toIntOrNull() ?: 0) - 1
}
val nowCalendar = Calendar.getInstance()
(nowCalendar[Calendar.DAY_OF_MONTH] == birthdayCalendar[Calendar.DAY_OF_MONTH]
&& nowCalendar[Calendar.MONTH] == birthdayCalendar[Calendar.MONTH])
} else false
}
fun mapToPresentation() = VkConversationUi(
conversationId = id,
lastMessageId = lastMessageId,
avatar = extractAvatar(),
title = extractTitle(),
unreadCount = extractUnreadCounterText(),
date = extractDate(),
message = extractMessage(),
attachmentImage = extractAttachmentImage(),
isPinned = majorId > 0,
actionState = ActionState.parse(isPhantom, isCallInProgress),
isBirthday = extractBirthday(),
isUnread = extractReadCondition(),
isAccount = isAccount(),
isOnline = !isAccount() && conversationUser?.online == true,
lastMessage = lastMessage,
conversationUser = conversationUser,
conversationGroup = conversationGroup,
peerType = peerType
)
}
@@ -1,33 +0,0 @@
package com.meloda.fast.api.model.presentation
import com.meloda.fast.api.model.ActionState
import com.meloda.fast.api.model.ConversationPeerType
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.base.AdapterDiffItem
import com.meloda.fast.model.base.UiImage
import com.meloda.fast.model.base.UiText
data class VkConversationUi(
val conversationId: Int,
val lastMessageId: Int,
val avatar: UiImage,
val title: UiText,
val unreadCount: String?,
val date: String,
val message: String,
val attachmentImage: UiImage?,
val isPinned: Boolean,
val actionState: ActionState,
val isBirthday: Boolean,
val isUnread: Boolean,
val isAccount: Boolean,
val isOnline: Boolean,
val lastMessage: VkMessage?,
val conversationUser: VkUser?,
val conversationGroup: VkGroup?,
val peerType: ConversationPeerType,
) : AdapterDiffItem {
override val id = conversationId
}
@@ -1,110 +0,0 @@
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
const val AccessTokenExpired = 1117
}
object VkErrors {
const val Unknown = "unknown_error"
const val NeedValidation = "need_validation"
const val NeedCaptcha = "need_captcha"
const val InvalidRequest = "invalid_request"
}
object VkErrorTypes {
const val OtpFormatIncorrect = "otp_format_is_incorrect"
const val WrongOtp = "wrong_otp"
}
object VkErrorMessages {
const val UserBanned = "user has been banned"
}
open class AuthorizationError : ApiError()
class TokenExpiredError : AuthorizationError()
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()
object WrongTwoFaCodeFormatError : ApiError()
object WrongTwoFaCodeError : ApiError()
data class UserBannedError(
@SerializedName("ban_info")
val banInfo: BanInfo
) : ApiError() {
data class BanInfo(
@SerializedName("member_name")
val memberName: String,
val message: String,
@SerializedName("access_token")
val accessToken: String,
@SerializedName("restore_url")
val restoreUrl: String
)
}
@@ -1,32 +0,0 @@
package com.meloda.fast.api.network
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.network.account.AccountUrls
import com.meloda.fast.api.network.ota.OtaUrls
import okhttp3.Interceptor
import okhttp3.Response
import java.net.URLEncoder
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().url.newBuilder()
val url = builder.build().toUrl().toString()
if (!url.contains("upload.php") && !url.contains(OtaUrls.GetActualUrl)) {
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
}
if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) {
UserConfig.accessToken.let {
if (it.isNotBlank())
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
}
}
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
}
}
@@ -1,155 +0,0 @@
@file:Suppress("UNCHECKED_CAST")
package com.meloda.fast.api.network
import com.google.gson.Gson
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.base.ApiError
import com.meloda.fast.api.base.ApiResponse
import okhttp3.Request
import okio.Timeout
import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): CallAdapter<*, *>? {
val rawReturnType: Class<*> = getRawType(returnType)
if (rawReturnType == Call::class.java) {
if (returnType is ParameterizedType) {
val callInnerType: Type = getParameterUpperBound(0, returnType)
if (getRawType(callInnerType) == ApiAnswer::class.java) {
if (callInnerType is ParameterizedType) {
val resultInnerType = getParameterUpperBound(0, callInnerType)
return ResultCallAdapter<Any?>(resultInnerType, gson)
}
return ResultCallAdapter<Nothing>(Nothing::class.java, gson)
}
}
}
return null
}
}
internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : Call<Out> {
override fun execute(): Response<Out> = throw NotImplementedError()
final override fun enqueue(callback: Callback<Out>) = enqueueImpl(callback)
final override fun clone(): Call<Out> = cloneImpl()
override fun cancel() = proxy.cancel()
override fun request(): Request = proxy.request()
override fun isExecuted() = proxy.isExecuted
override fun isCanceled() = proxy.isCanceled
abstract fun enqueueImpl(callback: Callback<Out>)
abstract fun cloneImpl(): Call<Out>
}
private class ResultCallAdapter<R>(private val type: Type, private val gson: Gson) : CallAdapter<R, Call<ApiAnswer<R>>> {
override fun responseType() = type
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call, gson)
}
internal class ResultCall<T>(proxy: Call<T>, private val gson: Gson) : CallDelegate<T, ApiAnswer<T>>(proxy) {
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
proxy.enqueue(ResultCallback(this, callback, gson))
}
override fun cloneImpl(): ResultCall<T> {
return ResultCall(proxy.clone(), gson)
}
private class ResultCallback<T>(
private val proxy: ResultCall<T>,
private val callback: Callback<ApiAnswer<T>>,
private val gson: Gson
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val result: ApiAnswer<T> =
if (response.isSuccessful) {
val baseBody = response.body()
if (baseBody !is ApiResponse<*>) {
ApiAnswer.Success(baseBody as T)
} else {
val body = baseBody as? ApiResponse<*>
if (body?.error != null) {
VkUtils.getApiError(gson, gson.toJson(body.error))
} else {
ApiAnswer.Success(body as T)
}
}
} else {
val errorBodyString = response.errorBody()?.string()
VkUtils.getApiError(gson, errorBodyString)
}
if (checkErrors(call, result)) {
return
}
callback.onResponse(proxy, Response.success(result))
}
override fun onFailure(call: Call<T>, error: Throwable) {
callback.onResponse(
proxy,
Response.success(ApiAnswer.Error(ApiError(throwable = error)))
)
}
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
if (result.isError()) {
result.error.throwable?.run {
onFailure(call, this)
return true
}
}
return false
}
}
override fun timeout(): Timeout {
return proxy.timeout()
}
}
sealed class ApiAnswer<out R> {
data class Success<out T>(val data: T) : ApiAnswer<T>()
data class Error(val error: ApiError) : ApiAnswer<Nothing>()
@OptIn(ExperimentalContracts::class)
fun isSuccessful(): Boolean {
contract {
returns(true) implies (this@ApiAnswer is Success)
}
return this is Success
}
@OptIn(ExperimentalContracts::class)
fun isError(): Boolean {
contract {
returns(true) implies (this@ApiAnswer is Error)
}
return this is Error
}
}

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