Compare commits

..

3 commits

Author SHA1 Message Date
72b82fa4e4
Merge branch 'nm17/wip' 2023-06-05 22:25:08 +04:00
40a2a755a8
feat: доработка API 2023-06-05 22:20:39 +04:00
dd7109786e feat: начало работы над клиентом 2023-06-04 00:47:20 +04:00
20 changed files with 461 additions and 59 deletions

View file

@ -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,5 +123,25 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
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")
}

View file

@ -2,6 +2,9 @@
<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.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -11,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"
@ -29,6 +28,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -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))

View file

@ -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")
}
}

View file

@ -0,0 +1,7 @@
package ru.nm17.narodmon.appNarodMonApiClient
/**
* Прятать данное значение нет смысла, ибо так или иначе
* кто-нибудь догадается его зареверсить в готовых сборках.
*/
const val API_KEY = "45z3du2MZY0vW"

View file

@ -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)
}
}

View file

@ -0,0 +1,6 @@
package ru.nm17.narodmon.appNarodMonApiClient.types
data class APIError(
var errno: Int,
var error: String
)

View file

@ -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
)

View file

@ -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
}
}

View file

@ -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?,
)

View file

@ -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)
}

View 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,
)

View file

@ -29,11 +29,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 {
@ -83,7 +83,8 @@ fun AgreementDialog(onClick: () -> Unit) {
onClick = { exitProcess(0) }) {
Text(text = stringResource(id = R.string.exit))
}
}
},
modifier = modifier
)
}

View file

@ -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,11 +29,15 @@ 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.NavHost
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
@ -42,16 +51,29 @@ 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)
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 = { navController.navigate("sensors") }
onClick = { /*TODO*/ }
)
}
}
}) {
Scaffold(
topBar = {

View 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-----

View 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>

View file

@ -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()
}
}

View file

@ -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

View file

@ -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" }

View file

@ -13,5 +13,6 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Народный Мониторинг"
include(":app")