Compare commits
4 commits
8a9ddbc27c
...
e384ffdf6c
Author | SHA1 | Date | |
---|---|---|---|
e384ffdf6c | |||
72b82fa4e4 | |||
40a2a755a8 | |||
dd7109786e |
20 changed files with 460 additions and 59 deletions
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
|
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.androidApplication)
|
||||||
|
@ -7,6 +6,7 @@ plugins {
|
||||||
kotlin("plugin.serialization") version "1.8.21"
|
kotlin("plugin.serialization") version "1.8.21"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "ru.nm17.narodmon"
|
namespace = "ru.nm17.narodmon"
|
||||||
compileSdk = 33
|
compileSdk = 33
|
||||||
|
@ -27,7 +27,10 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -62,6 +65,7 @@ dependencies {
|
||||||
implementation(libs.material3)
|
implementation(libs.material3)
|
||||||
implementation(libs.androidx.datastore.core.android)
|
implementation(libs.androidx.datastore.core.android)
|
||||||
implementation(libs.androidx.room.common)
|
implementation(libs.androidx.room.common)
|
||||||
|
implementation(libs.androidx.security.crypto.ktx)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||||
androidTestImplementation(libs.espresso.core)
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
@ -119,11 +123,32 @@ dependencies {
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||||
|
|
||||||
|
|
||||||
// Ktor
|
// Ktor
|
||||||
val ktor_version = "2.3.1"
|
val ktor_version = "2.3.1"
|
||||||
implementation("io.ktor:ktor-client-core:$ktor_version")
|
implementation("io.ktor:ktor-client-core:$ktor_version")
|
||||||
implementation("io.ktor:ktor-client-cio:$ktor_version")
|
implementation("io.ktor:ktor-client-cio:$ktor_version")
|
||||||
|
|
||||||
|
implementation(platform("dev.forkhandles:forkhandles-bom:2.6.0.0"))
|
||||||
|
implementation("dev.forkhandles:result4k")
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-core:2.3.1")
|
||||||
|
implementation("io.ktor:ktor-client-okhttp:2.3.1")
|
||||||
|
|
||||||
|
implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06")
|
||||||
|
|
||||||
|
// For Identity Credential APIs
|
||||||
|
implementation("androidx.security:security-identity-credential:1.0.0-alpha03")
|
||||||
|
|
||||||
|
// For App Authentication APIs
|
||||||
|
implementation("androidx.security:security-app-authenticator:1.0.0-alpha02")
|
||||||
|
|
||||||
|
// For App Authentication API testing
|
||||||
|
androidTestImplementation("androidx.security:security-app-authenticator:1.0.0-alpha01")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
|
|
||||||
// Map Compose library
|
// Map Compose library
|
||||||
implementation("ovh.plrapps:mapcompose:2.7.1")
|
implementation("ovh.plrapps:mapcompose:2.7.1")
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -13,12 +14,8 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.NarodMon"
|
android:theme="@style/Theme.NarodMon"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<service
|
|
||||||
android:name=".DataStoreService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:process=":data_store_process"
|
|
||||||
android:exported="false"></service>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
@ -31,6 +28,9 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -4,47 +4,19 @@
|
||||||
|
|
||||||
package ru.nm17.narodmon
|
package ru.nm17.narodmon
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
|
||||||
import androidx.compose.material.icons.filled.Menu
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.DrawerValue
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
|
||||||
import androidx.compose.material3.NavigationDrawerItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberDrawerState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -52,34 +24,23 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKeys
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.single
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.nm17.narodmon.db.AppDatabase
|
import ru.nm17.narodmon.db.AppDatabase
|
||||||
import ru.nm17.narodmon.db.entities.KVSetting
|
import ru.nm17.narodmon.db.entities.KVSetting
|
||||||
import ru.nm17.narodmon.ui.elements.AgreementDialog
|
import ru.nm17.narodmon.ui.elements.AgreementDialog
|
||||||
import ru.nm17.narodmon.ui.elements.GenericNavScaffold
|
|
||||||
import ru.nm17.narodmon.ui.pages.SensorsPage
|
import ru.nm17.narodmon.ui.pages.SensorsPage
|
||||||
import ru.nm17.narodmon.ui.theme.NarodMonTheme
|
import ru.nm17.narodmon.ui.theme.NarodMonTheme
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost() {
|
fun AppNavHost() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
@ -107,6 +68,22 @@ class MainActivity : ComponentActivity() {
|
||||||
AppDatabase::class.java, "data"
|
AppDatabase::class.java, "data"
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
|
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||||
|
|
||||||
|
val sharedPreferences = EncryptedSharedPreferences.create(
|
||||||
|
"secret_shared_prefs",
|
||||||
|
masterKeyAlias,
|
||||||
|
createDeviceProtectedStorageContext(),
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
|
||||||
|
// use the shared preferences and editor as you normally would
|
||||||
|
|
||||||
|
// use the shared preferences and editor as you normally would
|
||||||
|
val credSharedPreferences = sharedPreferences
|
||||||
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val coScope = rememberCoroutineScope()
|
val coScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
@ -134,7 +111,9 @@ class MainActivity : ComponentActivity() {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(it)
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
Text(text = stringResource(R.string.waiting_for_user_agreement))
|
Text(text = stringResource(R.string.waiting_for_user_agreement))
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
private class PermissionSlip
|
||||||
|
|
||||||
|
class RateLimitingSemaphore<T>(
|
||||||
|
val availablePermits: AtomicInteger,
|
||||||
|
var lastPermitHanded: AtomicInteger,
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun acquire() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
fun release() {
|
||||||
|
val current = availablePermits.getAcquire()
|
||||||
|
if (current > 0) {
|
||||||
|
availablePermits.setRelease(current - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryAcquire(): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Прятать данное значение нет смысла, ибо так или иначе
|
||||||
|
* кто-нибудь догадается его зареверсить в готовых сборках.
|
||||||
|
*/
|
||||||
|
const val API_KEY = "45z3du2MZY0vW"
|
|
@ -0,0 +1,151 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient
|
||||||
|
|
||||||
|
import dev.forkhandles.result4k.Failure
|
||||||
|
import dev.forkhandles.result4k.Result4k
|
||||||
|
import dev.forkhandles.result4k.Success
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import io.ktor.http.headers
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import ru.nm17.narodmon.appNarodMonApiClient.types.APIError
|
||||||
|
import ru.nm17.narodmon.appNarodMonApiClient.types.AppInitRequest
|
||||||
|
import ru.nm17.narodmon.appNarodMonApiClient.types.AppInitResponse
|
||||||
|
import ru.nm17.narodmon.appNarodMonApiClient.types.MandatoryParams
|
||||||
|
import ru.nm17.narodmon.db.AppDatabase
|
||||||
|
import ru.nm17.narodmon.db.entities.KVSetting
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
const val allowedUuidChars = "abcdef0123456789"
|
||||||
|
const val uuidCharLength = 16
|
||||||
|
|
||||||
|
private val apiHttpClient = HttpClient(OkHttp) {
|
||||||
|
headers {
|
||||||
|
append(HttpHeaders.UserAgent, "IoTMonitor")
|
||||||
|
append(HttpHeaders.AcceptEncoding, "br;q=1.0, gzip;q=0.6, deflate;q=0.6")
|
||||||
|
}
|
||||||
|
engine {
|
||||||
|
// this: OkHttpConfig
|
||||||
|
config {
|
||||||
|
// this: OkHttpClient.Builder
|
||||||
|
followRedirects(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client private constructor(
|
||||||
|
private var uuid: String,
|
||||||
|
private var _rawHttpClient: HttpClient
|
||||||
|
) {
|
||||||
|
private val lang: String = Locale.getDefault().language
|
||||||
|
|
||||||
|
val httpClient1Min = flow {
|
||||||
|
while (true) {
|
||||||
|
emit(_rawHttpClient)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromDb(db: AppDatabase): Flow<Client> {
|
||||||
|
return flow {
|
||||||
|
val uuid = db.kvDao().getByKey("current_user_uuid")?.value
|
||||||
|
|
||||||
|
if (uuid == null) {
|
||||||
|
val newUuid =
|
||||||
|
List(uuidCharLength) { allowedUuidChars.random() }.joinToString("")
|
||||||
|
db.kvDao().setAll(KVSetting("current_user_uuid", newUuid))
|
||||||
|
db.kvDao().setAll(
|
||||||
|
KVSetting(
|
||||||
|
"current_user_uuid_generated_at_unix_ms_utc",
|
||||||
|
Json.encodeToString(Clock.System.now().epochSeconds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
emit(Client(newUuid, apiHttpClient))
|
||||||
|
} else {
|
||||||
|
emit(Client(uuid, apiHttpClient))
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMandatoryParams(cmd: String): MandatoryParams {
|
||||||
|
return MandatoryParams(
|
||||||
|
cmd,
|
||||||
|
this@Client.lang,
|
||||||
|
this@Client.uuid,
|
||||||
|
API_KEY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getJSONRequestBody(cmd: String, request: @Serializable Any) {
|
||||||
|
mergeJsonObjects(
|
||||||
|
Json.encodeToJsonElement(request).jsonObject,
|
||||||
|
Json.encodeToJsonElement(
|
||||||
|
getMandatoryParams(cmd)
|
||||||
|
).jsonObject
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <reified RT> deserializeResponse(resp: HttpResponse): Result4k<RT, APIError> {
|
||||||
|
val body: String = resp.body()
|
||||||
|
return try {
|
||||||
|
Success(Json.decodeFromString(body))
|
||||||
|
} catch (err: SerializationException) {
|
||||||
|
Failure(Json.decodeFromString(body))
|
||||||
|
} catch (err: Exception) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun appInit(request: AppInitRequest): Flow<Result4k<AppInitResponse, APIError>> {
|
||||||
|
val client = this.httpClient1Min
|
||||||
|
return flow<Result4k<AppInitResponse, APIError>> {
|
||||||
|
|
||||||
|
val resp = client.post("https://narodmon.ru/api") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
getJSONRequestBody("appInit", request)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(deserializeResponse(resp))
|
||||||
|
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
}
|
||||||
|
public fun appInit(request: AppInitRequest): Flow<Result4k<AppInitResponse, APIError>> {
|
||||||
|
val client = this.httpClient1Min
|
||||||
|
return flow<Result4k<AppInitResponse, APIError>> {
|
||||||
|
|
||||||
|
val resp = client.post("https://narodmon.ru/api") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
getJSONRequestBody("appInit", request)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(deserializeResponse(resp))
|
||||||
|
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient.types
|
||||||
|
|
||||||
|
data class APIError(
|
||||||
|
var errno: Int,
|
||||||
|
var error: String
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient.types
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AppInitRequest (
|
||||||
|
@JsonNames("version")
|
||||||
|
val appVersion: String,
|
||||||
|
val platform: String,
|
||||||
|
val model: String,
|
||||||
|
val width: Int,
|
||||||
|
val utc: Int
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient.types
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class VipStatus(var value: Int) {
|
||||||
|
IsVip(1),
|
||||||
|
NotVip(0)
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
data class SensorTypes(
|
||||||
|
@SerialName("type")
|
||||||
|
var typeCode: Long,
|
||||||
|
var name: String,
|
||||||
|
var types: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AppInitResponse(
|
||||||
|
var latest: String?,
|
||||||
|
var url: String?,
|
||||||
|
var login: String,
|
||||||
|
var vip: Boolean, // TODO: Change this if it doesn't work
|
||||||
|
var lat: Long,
|
||||||
|
var long: Double,
|
||||||
|
var addr: Double,
|
||||||
|
/**
|
||||||
|
* Timestamp в секундах спустя Epoch по часовой зоне пользователя
|
||||||
|
*/
|
||||||
|
var timestamp: Long,
|
||||||
|
var types: Array<SensorTypes>,
|
||||||
|
var favorites: Array<Long>
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as AppInitResponse
|
||||||
|
|
||||||
|
if (latest != other.latest) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (login != other.login) return false
|
||||||
|
if (vip != other.vip) return false
|
||||||
|
if (lat != other.lat) return false
|
||||||
|
if (long != other.long) return false
|
||||||
|
if (addr != other.addr) return false
|
||||||
|
if (timestamp != other.timestamp) return false
|
||||||
|
if (!types.contentEquals(other.types)) return false
|
||||||
|
return favorites.contentEquals(other.favorites)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = latest.hashCode()
|
||||||
|
result = 31 * result + url.hashCode()
|
||||||
|
result = 31 * result + login.hashCode()
|
||||||
|
result = 31 * result + vip.hashCode()
|
||||||
|
result = 31 * result + lat.hashCode()
|
||||||
|
result = 31 * result + long.hashCode()
|
||||||
|
result = 31 * result + addr.hashCode()
|
||||||
|
result = 31 * result + timestamp.hashCode()
|
||||||
|
result = 31 * result + types.contentHashCode()
|
||||||
|
result = 31 * result + favorites.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient.types
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class NarodMonLanguages(val value: String) {
|
||||||
|
Russian("ru"),
|
||||||
|
English("en"),
|
||||||
|
Ukrainian("uk")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MandatoryParams(
|
||||||
|
var cmd: String?,
|
||||||
|
var lang: String,
|
||||||
|
var uuid: String,
|
||||||
|
var apiKey: String?,
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.nm17.narodmon.appNarodMonApiClient
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import ru.nm17.narodmon.appNarodMonApiClient.types.MandatoryParams
|
||||||
|
|
||||||
|
fun mergeJsonObjects(json1: JsonObject, json2: JsonObject): JsonObject {
|
||||||
|
val result = mutableMapOf<String, kotlinx.serialization.json.JsonElement>()
|
||||||
|
result.putAll(json1)
|
||||||
|
json2.forEach { (key, value) ->
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return JsonObject(result)
|
||||||
|
}
|
||||||
|
|
11
app/src/main/java/ru/nm17/narodmon/db/entities/SensorType.kt
Normal file
11
app/src/main/java/ru/nm17/narodmon/db/entities/SensorType.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package ru.nm17.narodmon.db.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "settings")
|
||||||
|
data class SensorType(
|
||||||
|
@PrimaryKey val code: Long,
|
||||||
|
val name: String,
|
||||||
|
val unit: String,
|
||||||
|
)
|
|
@ -24,11 +24,11 @@ import ru.nm17.narodmon.R
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgreementDialog(onClick: () -> Unit) {
|
fun AgreementDialog(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { /* Ничего не делаем пока пользователь не примет соглашение. */ },
|
onDismissRequest = { /* Делаем ничего пока пользователь не примет соглашение. */ },
|
||||||
title = { Text(text = stringResource(id = R.string.agreement_dialog_title)) },
|
title = { Text(text = stringResource(id = R.string.agreement_dialog_title)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
|
@ -78,7 +78,8 @@ fun AgreementDialog(onClick: () -> Unit) {
|
||||||
onClick = { exitProcess(0) }) {
|
onClick = { exitProcess(0) }) {
|
||||||
Text(text = stringResource(id = R.string.exit))
|
Text(text = stringResource(id = R.string.exit))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = modifier
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
package ru.nm17.narodmon.ui.elements
|
package ru.nm17.narodmon.ui.elements
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
@ -14,6 +18,7 @@ import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
|
@ -24,10 +29,14 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,15 +50,28 @@ fun GenericNavScaffold(title: @Composable () -> Unit, content: @Composable (Padd
|
||||||
val coScope = rememberCoroutineScope()
|
val coScope = rememberCoroutineScope()
|
||||||
val navController = rememberNavController() // TODO: Используй меня
|
val navController = rememberNavController() // TODO: Используй меня
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = Unit) {
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(drawerState = expanded, drawerContent = {
|
ModalNavigationDrawer(drawerState = expanded, drawerContent = {
|
||||||
ModalDrawerSheet {
|
ModalDrawerSheet {
|
||||||
Text("Drawer title", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge)
|
ListItem(
|
||||||
Divider()
|
leadingContent = {
|
||||||
NavigationDrawerItem(
|
Icon(Icons.Default.AccountCircle, contentDescription = "")
|
||||||
label = { Text(text = "Drawer Item") },
|
},
|
||||||
selected = true,
|
headlineText = { Text(text = "Гость", style = MaterialTheme.typography.titleLarge)},
|
||||||
onClick = { navController.navigate("sensors") }
|
modifier = Modifier.height(72.dp)
|
||||||
)
|
)
|
||||||
|
Divider()
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp)) {
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = { Text(text = "Drawer Item") },
|
||||||
|
selected = true,
|
||||||
|
onClick = { /*TODO*/ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
31
app/src/main/res/raw/isrgrootx1.pem
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||||
|
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||||
|
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||||
|
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||||
|
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||||
|
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||||
|
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||||
|
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||||
|
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||||
|
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||||
|
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||||
|
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||||
|
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||||
|
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||||
|
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||||
|
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||||
|
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||||
|
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||||
|
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||||
|
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||||
|
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||||
|
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||||
|
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||||
|
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||||
|
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||||
|
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||||
|
-----END CERTIFICATE-----
|
9
app/src/main/res/xml/network_security_config.xml
Normal file
9
app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config>
|
||||||
|
<domain includeSubdomains="true">narodmon.ru</domain>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="@raw/isrgrootx1"/>
|
||||||
|
</trust-anchors>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
|
@ -3,6 +3,7 @@ package ru.nm17.narodmon
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
@ -13,5 +14,6 @@ class ExampleUnitTest {
|
||||||
@Test
|
@Test
|
||||||
fun addition_isCorrect() {
|
fun addition_isCorrect() {
|
||||||
assertEquals(4, 2 + 2)
|
assertEquals(4, 2 + 2)
|
||||||
|
URL("https://tile.openstreetmap.org/${zoom}/${row}/${col}").openStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,4 +6,5 @@ plugins {
|
||||||
id("com.google.devtools.ksp") version "1.8.21-1.0.11" apply false
|
id("com.google.devtools.ksp") version "1.8.21-1.0.11" apply false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true // Needed to make the Suppress annotation work for the plugins block
|
true // Needed to make the Suppress annotation work for the plugins block
|
|
@ -11,6 +11,7 @@ compose-bom = "2023.03.00"
|
||||||
navigation-fragment = "2.5.3"
|
navigation-fragment = "2.5.3"
|
||||||
datastore-core-android = "1.1.0-alpha04"
|
datastore-core-android = "1.1.0-alpha04"
|
||||||
room-common = "2.5.1"
|
room-common = "2.5.1"
|
||||||
|
security-crypto-ktx = "1.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigation-fragment" }
|
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigation-fragment" }
|
||||||
|
@ -30,6 +31,7 @@ ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
material3 = { group = "androidx.compose.material3", name = "material3" }
|
material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastore-core-android" }
|
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastore-core-android" }
|
||||||
androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "room-common" }
|
androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "room-common" }
|
||||||
|
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security-crypto-ktx" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
@ -13,5 +13,6 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
rootProject.name = "Народный Мониторинг"
|
rootProject.name = "Народный Мониторинг"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
Loading…
Add table
Reference in a new issue