diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f7f7827..9a73f7f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c7e740..4a6a90d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + - + + + \ No newline at end of file diff --git a/app/src/main/java/ru/nm17/narodmon/MainActivity.kt b/app/src/main/java/ru/nm17/narodmon/MainActivity.kt index c41c7b3..63b65e5 100644 --- a/app/src/main/java/ru/nm17/narodmon/MainActivity.kt +++ b/app/src/main/java/ru/nm17/narodmon/MainActivity.kt @@ -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)) diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/RateLimitingSemaphore.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/RateLimitingSemaphore.kt new file mode 100644 index 0000000..f35881a --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/RateLimitingSemaphore.kt @@ -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( + 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/api_key.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/api_key.kt new file mode 100644 index 0000000..20e4c2e --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/api_key.kt @@ -0,0 +1,7 @@ +package ru.nm17.narodmon.appNarodMonApiClient + +/** + * Прятать данное значение нет смысла, ибо так или иначе + * кто-нибудь догадается его зареверсить в готовых сборках. + */ +const val API_KEY = "45z3du2MZY0vW" \ No newline at end of file diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/client.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/client.kt new file mode 100644 index 0000000..ec594fe --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/client.kt @@ -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 { + 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 deserializeResponse(resp: HttpResponse): Result4k { + 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> { + val client = this.httpClient1Min + return flow> { + + 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> { + val client = this.httpClient1Min + return flow> { + + val resp = client.post("https://narodmon.ru/api") { + contentType(ContentType.Application.Json) + setBody( + getJSONRequestBody("appInit", request) + ) + } + + emit(deserializeResponse(resp)) + + }.flowOn(Dispatchers.IO) + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/APIError.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/APIError.kt new file mode 100644 index 0000000..f20516b --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/APIError.kt @@ -0,0 +1,6 @@ +package ru.nm17.narodmon.appNarodMonApiClient.types + +data class APIError( + var errno: Int, + var error: String +) diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitRequest.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitRequest.kt new file mode 100644 index 0000000..d83b3a9 --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitRequest.kt @@ -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 +) + diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitResponse.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitResponse.kt new file mode 100644 index 0000000..289cfd9 --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/AppInitResponse.kt @@ -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, + var favorites: Array +) { + 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 + } +} diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/MandatoryParams.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/MandatoryParams.kt new file mode 100644 index 0000000..45db650 --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/types/MandatoryParams.kt @@ -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?, +) diff --git a/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/utils.kt b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/utils.kt new file mode 100644 index 0000000..cd42c90 --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/appNarodMonApiClient/utils.kt @@ -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() + result.putAll(json1) + json2.forEach { (key, value) -> + result[key] = value + } + return JsonObject(result) +} + diff --git a/app/src/main/java/ru/nm17/narodmon/db/entities/SensorType.kt b/app/src/main/java/ru/nm17/narodmon/db/entities/SensorType.kt new file mode 100644 index 0000000..006ecce --- /dev/null +++ b/app/src/main/java/ru/nm17/narodmon/db/entities/SensorType.kt @@ -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, +) diff --git a/app/src/main/java/ru/nm17/narodmon/ui/elements/AgreementDialog.kt b/app/src/main/java/ru/nm17/narodmon/ui/elements/AgreementDialog.kt index 8085739..25d8c97 100644 --- a/app/src/main/java/ru/nm17/narodmon/ui/elements/AgreementDialog.kt +++ b/app/src/main/java/ru/nm17/narodmon/ui/elements/AgreementDialog.kt @@ -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 ) } \ No newline at end of file diff --git a/app/src/main/java/ru/nm17/narodmon/ui/elements/Scaffolds.kt b/app/src/main/java/ru/nm17/narodmon/ui/elements/Scaffolds.kt index ac1aa09..016df5f 100644 --- a/app/src/main/java/ru/nm17/narodmon/ui/elements/Scaffolds.kt +++ b/app/src/main/java/ru/nm17/narodmon/ui/elements/Scaffolds.kt @@ -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( diff --git a/app/src/main/res/raw/isrgrootx1.pem b/app/src/main/res/raw/isrgrootx1.pem new file mode 100644 index 0000000..b85c803 --- /dev/null +++ b/app/src/main/res/raw/isrgrootx1.pem @@ -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----- diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..360b762 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + narodmon.ru + + + + + diff --git a/app/src/test/java/ru/nm17/narodmon/ExampleUnitTest.kt b/app/src/test/java/ru/nm17/narodmon/ExampleUnitTest.kt index 8660651..0a47494 100644 --- a/app/src/test/java/ru/nm17/narodmon/ExampleUnitTest.kt +++ b/app/src/test/java/ru/nm17/narodmon/ExampleUnitTest.kt @@ -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() } } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b82cb02..05559cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f788ece..a952870 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index db4a857..718a0ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,5 +13,6 @@ dependencyResolutionManagement { } } + rootProject.name = "Народный Мониторинг" include(":app")