dc09-sensors #4
20 changed files with 460 additions and 59 deletions
|
@ -1,4 +1,3 @@
|
|||
|
||||
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
|
@ -7,6 +6,7 @@ plugins {
|
|||
kotlin("plugin.serialization") version "1.8.21"
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace = "ru.nm17.narodmon"
|
||||
compileSdk = 33
|
||||
|
@ -27,7 +27,10 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
@ -62,6 +65,7 @@ dependencies {
|
|||
implementation(libs.material3)
|
||||
implementation(libs.androidx.datastore.core.android)
|
||||
implementation(libs.androidx.room.common)
|
||||
implementation(libs.androidx.security.crypto.ktx)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
@ -119,11 +123,32 @@ dependencies {
|
|||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
|
||||
|
||||
// Ktor
|
||||
val ktor_version = "2.3.1"
|
||||
implementation("io.ktor:ktor-client-core:$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
|
||||
implementation("ovh.plrapps:mapcompose:2.7.1")
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -13,12 +14,8 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.NarodMon"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="31">
|
||||
<service
|
||||
android:name=".DataStoreService"
|
||||
android:enabled="true"
|
||||
android:process=":data_store_process"
|
||||
android:exported="false"></service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
@ -31,6 +28,9 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -4,47 +4,19 @@
|
|||
|
||||
package ru.nm17.narodmon
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.Divider
|
||||
import androidx.compose.material3.DrawerValue
|
||||
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.Surface
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
@ -52,34 +24,23 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.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.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
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.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 ru.nm17.narodmon.db.AppDatabase
|
||||
import ru.nm17.narodmon.db.entities.KVSetting
|
||||
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.theme.NarodMonTheme
|
||||
|
||||
|
||||
@Composable
|
||||
fun AppNavHost() {
|
||||
val navController = rememberNavController()
|
||||
|
@ -107,6 +68,22 @@ class MainActivity : ComponentActivity() {
|
|||
AppDatabase::class.java, "data"
|
||||
).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 {
|
||||
val coScope = rememberCoroutineScope()
|
||||
|
||||
|
@ -134,7 +111,9 @@ class MainActivity : ComponentActivity() {
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(it)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun AgreementDialog(onClick: () -> Unit) {
|
||||
fun AgreementDialog(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { /* Ничего не делаем пока пользователь не примет соглашение. */ },
|
||||
onDismissRequest = { /* Делаем ничего пока пользователь не примет соглашение. */ },
|
||||
title = { Text(text = stringResource(id = R.string.agreement_dialog_title)) },
|
||||
text = {
|
||||
Column {
|
||||
|
@ -78,7 +78,8 @@ fun AgreementDialog(onClick: () -> Unit) {
|
|||
onClick = { exitProcess(0) }) {
|
||||
Text(text = stringResource(id = R.string.exit))
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
|
||||
)
|
||||
}
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
package ru.nm17.narodmon.ui.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.Divider
|
||||
|
@ -14,6 +18,7 @@ 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
|
||||
|
@ -24,10 +29,14 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
|
@ -41,15 +50,28 @@ fun GenericNavScaffold(title: @Composable () -> Unit, content: @Composable (Padd
|
|||
val coScope = rememberCoroutineScope()
|
||||
val navController = rememberNavController() // TODO: Используй меня
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(drawerState = expanded, drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Text("Drawer title", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge)
|
||||
Divider()
|
||||
NavigationDrawerItem(
|
||||
label = { Text(text = "Drawer Item") },
|
||||
selected = true,
|
||||
onClick = { navController.navigate("sensors") }
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(Icons.Default.AccountCircle, contentDescription = "")
|
||||
},
|
||||
headlineText = { Text(text = "Гость", style = MaterialTheme.typography.titleLarge)},
|
||||
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(
|
||||
|
|
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-----
|
||||
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
|
||||
-----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.Assert.*
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
|
@ -13,5 +14,6 @@ class ExampleUnitTest {
|
|||
@Test
|
||||
fun addition_isCorrect() {
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
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"
|
||||
datastore-core-android = "1.1.0-alpha04"
|
||||
room-common = "2.5.1"
|
||||
security-crypto-ktx = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
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-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security-crypto-ktx" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
@ -13,5 +13,6 @@ dependencyResolutionManagement {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
rootProject.name = "Народный Мониторинг"
|
||||
include(":app")
|
||||
|
|
Loading…
Reference in a new issue