summaryrefslogtreecommitdiff
path: root/benchmark/app
diff options
context:
space:
mode:
Diffstat (limited to 'benchmark/app')
-rw-r--r--benchmark/app/.gitignore1
-rw-r--r--benchmark/app/build.gradle.kts81
-rw-r--r--benchmark/app/proguard-rules.pro21
-rw-r--r--benchmark/app/src/androidTest/java/io/trentetroim/benchmark/ExampleInstrumentedTest.kt25
-rw-r--r--benchmark/app/src/main/AndroidManifest.xml48
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt274
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt90
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt100
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt811
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt37
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt16
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt24
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt20
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt164
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt69
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt38
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt43
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt17
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt55
-rw-r--r--benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt36
-rw-r--r--benchmark/app/src/main/res/drawable/card_bg.xml12
-rw-r--r--benchmark/app/src/main/res/drawable/ic_bench_icon.xml93
-rw-r--r--benchmark/app/src/main/res/drawable/ic_launcher_background.xml74
-rw-r--r--benchmark/app/src/main/res/drawable/ic_launcher_foreground.xml31
-rw-r--r--benchmark/app/src/main/res/drawable/list_handle_layout.xml12
-rw-r--r--benchmark/app/src/main/res/drawable/plus_5345954.pngbin0 -> 384 bytes
-rw-r--r--benchmark/app/src/main/res/drawable/target.xml9
-rw-r--r--benchmark/app/src/main/res/font/nokia_font.xml7
-rw-r--r--benchmark/app/src/main/res/font/nokiafc22.ttfbin0 -> 16272 bytes
-rw-r--r--benchmark/app/src/main/res/layout/bottom_sheet_dialog.xml29
-rw-r--r--benchmark/app/src/main/res/layout/card_layout.xml39
-rw-r--r--benchmark/app/src/main/res/layout/dialog_bench_details.xml109
-rw-r--r--benchmark/app/src/main/res/layout/dialog_review.xml40
-rw-r--r--benchmark/app/src/main/res/layout/dialog_reviews_list.xml40
-rw-r--r--benchmark/app/src/main/res/layout/item_review.xml26
-rw-r--r--benchmark/app/src/main/res/layout/main_layout.xml34
-rw-r--r--benchmark/app/src/main/res/layout/new_bench_layout.xml44
-rw-r--r--benchmark/app/src/main/res/mipmap-anydpi/ic_launcher.xml6
-rw-r--r--benchmark/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml6
-rw-r--r--benchmark/app/src/main/res/mipmap-hdpi/ic_launcher.webpbin0 -> 1404 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-hdpi/ic_launcher_round.webpbin0 -> 2898 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-mdpi/ic_launcher.webpbin0 -> 982 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-mdpi/ic_launcher_round.webpbin0 -> 1772 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1900 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webpbin0 -> 3918 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 2884 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webpbin0 -> 5914 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 3844 bytes
-rw-r--r--benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webpbin0 -> 7778 bytes
-rw-r--r--benchmark/app/src/main/res/values/colors.xml13
-rw-r--r--benchmark/app/src/main/res/values/strings.xml18
-rw-r--r--benchmark/app/src/main/res/values/themes.xml12
-rw-r--r--benchmark/app/src/main/res/xml/backup_rules.xml14
-rw-r--r--benchmark/app/src/main/res/xml/data_extraction_rules.xml20
-rw-r--r--benchmark/app/src/main/res/xml/network_security_config.xml7
-rw-r--r--benchmark/app/src/test/java/io/trentetroim/benchmark/ExampleUnitTest.kt17
56 files changed, 2682 insertions, 0 deletions
diff --git a/benchmark/app/.gitignore b/benchmark/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/benchmark/app/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/benchmark/app/build.gradle.kts b/benchmark/app/build.gradle.kts
new file mode 100644
index 0000000..7dfb21a
--- /dev/null
+++ b/benchmark/app/build.gradle.kts
@@ -0,0 +1,81 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "io.trentetroim.benchmark"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "io.trentetroim.benchmark"
+ minSdk = 33
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation(libs.osmdroid.android)
+ implementation(libs.osmbonuspack)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.cardview)
+
+ implementation(libs.retrofit)
+ implementation(libs.retrofit.gson)
+ implementation(libs.gson)
+
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.logging)
+
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.play.services.location)
+ implementation(libs.material)
+ implementation(libs.glide)
+ implementation(libs.samsung.image.picker)
+ implementation(libs.coil.compose)
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ implementation(libs.androidx.core.splashscreen)
+ debugImplementation(libs.androidx.ui.test.manifest)
+ implementation("com.github.a914-gowtham:compose-ratingbar:1.3.12")
+ implementation(libs.slidingUp){exclude(group="com.android.support", module="support-v4")}
+}
diff --git a/benchmark/app/proguard-rules.pro b/benchmark/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/benchmark/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/benchmark/app/src/androidTest/java/io/trentetroim/benchmark/ExampleInstrumentedTest.kt b/benchmark/app/src/androidTest/java/io/trentetroim/benchmark/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..a883705
--- /dev/null
+++ b/benchmark/app/src/androidTest/java/io/trentetroim/benchmark/ExampleInstrumentedTest.kt
@@ -0,0 +1,25 @@
+package io.trentetroim.benchmark
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext =
+ InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("io.trentetroim.benchmark", appContext.packageName)
+ }
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/AndroidManifest.xml b/benchmark/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9021931
--- /dev/null
+++ b/benchmark/app/src/main/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
+
+ <application
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@drawable/ic_bench_icon"
+ android:label="@string/app_name"
+ android:roundIcon="@drawable/ic_bench_icon"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppSplash"
+ android:networkSecurityConfig="@xml/network_security_config"
+ android:usesCleartextTraffic="true"
+ tools:replace="android:theme">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity android:name=".MapActivity"
+ android:exported="true"
+ android:theme="@style/Theme.Benchmark">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ </intent-filter>
+ </activity>
+ <activity android:name=".AddBenchActivity"
+ android:exported="true"
+ android:theme="@style/Theme.Benchmark">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt
new file mode 100644
index 0000000..62432e3
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/AddBenchActivity.kt
@@ -0,0 +1,274 @@
+package io.trentetroim.benchmark
+
+import android.app.Activity
+import android.graphics.drawable.shapes.Shape
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import coil.compose.rememberAsyncImagePainter
+import com.gowtham.ratingbar.RatingBar
+import com.gowtham.ratingbar.RatingBarStyle
+import io.trentetroim.benchmark.ui.theme.BenchmarkTheme
+import io.trentetroim.benchmark.ui.theme.Grass
+import io.trentetroim.benchmark.ui.theme.Typography
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.repository.PointRepository
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.launch
+import androidx.lifecycle.lifecycleScope
+import io.trentetroim.benchmark.ui.theme.Lagoon
+import io.trentetroim.benchmark.ui.theme.Tildeeth
+import io.trentetroim.benchmark.ui.theme.TildeethAlpha
+import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
+
+class AddBenchActivity : ComponentActivity() {
+ private val defaultImageUrl = "https://guillotinemelody.dev/resources/icon.png"
+ private val pointRepository = PointRepository()
+ private val reviewRepository = ReviewRepository()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ installSplashScreen()
+ val latitude = intent.getDoubleExtra("currentLatitude", 0.0)
+ val longitude = intent.getDoubleExtra("currentLongitude", 0.0)
+
+ setContent {
+ BenchmarkTheme {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxSize().background(
+ color = Grass
+ ).padding(16.dp)
+ ) {
+ Text(text = stringResource(R.string.new_bench), textAlign = TextAlign.Center)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var description by remember { mutableStateOf("") }
+ Input(value = description, onValueChange = { description = it })
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var rating by remember { mutableFloatStateOf(3.0f) }
+ Rating(value = rating, onValueChange = { rating = it })
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ var imageUrl by remember { mutableStateOf(defaultImageUrl) }
+ var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
+ var isUploading by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+
+ val pickImageLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.GetContent()
+ ) { uri: Uri? ->
+ uri?.let {
+ selectedImageUri = it
+ isUploading = true
+
+ lifecycleScope.launch {
+ pointRepository.uploadImage(context, it).collect { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ imageUrl = response.data
+ isUploading = false
+ Toast.makeText(context, "Image uploaded successfully", Toast.LENGTH_SHORT).show()
+ }
+ is ApiResponse.Error -> {
+ isUploading = false
+ Toast.makeText(context, "Error: ${response.errorMessage}", Toast.LENGTH_SHORT).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Button(
+ onClick = { pickImageLauncher.launch("image/*") },
+ enabled = !isUploading
+ ) {
+ Text("Ajouter une photo")
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ if (isUploading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = Color.White
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ selectedImageUri?.let {
+ Image(
+ painter = rememberAsyncImagePainter(it),
+ contentDescription = "Selected image",
+ modifier = Modifier
+ .size(150.dp)
+ .padding(8.dp)
+ )
+ }
+
+ Text(
+ text = "URL de l'image: $imageUrl",
+ modifier = Modifier.padding(8.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Button(onClick = { finish() }) {
+ Text(stringResource(R.string.cancel))
+ }
+
+ Button(onClick = {
+ val prefixedImageUrl = if (!imageUrl.startsWith("http://la-banquise.fr:5431")) {
+ "http://la-banquise.fr:5431" + if (imageUrl.startsWith("/")) imageUrl else "/$imageUrl"
+ } else {
+ imageUrl
+ }
+
+ val point = Point(
+ x = latitude,
+ y = longitude,
+ type = "bench",
+ name = description,
+ picture = prefixedImageUrl,
+ id = ""
+ )
+
+ lifecycleScope.launch {
+ pointRepository.createPoint(point).collect { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ Log.d("AddBenchActivity", "Point created: $point")
+
+ val createdPoint = response.data
+ val initialReview = Review(
+ id = "",
+ pointId = createdPoint.id,
+ grade = rating.toInt(),
+ comment = "Initial rating",
+ pictures = null
+ )
+
+ reviewRepository.addReview(createdPoint.id, initialReview).collect { reviewResponse ->
+ when (reviewResponse) {
+ is ApiResponse.Success -> {
+ Log.d("AddBenchActivity", "Initial review added: ${reviewResponse.data}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Banc et note initiale ajoutés avec succès!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Error -> {
+ Log.e("AddBenchActivity", "Error adding initial review: ${reviewResponse.errorMessage}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Banc ajouté mais erreur lors de l'ajout de la note initiale",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ finish()
+ }
+ }
+ is ApiResponse.Error -> {
+ Log.e("AddBenchActivity", "Error creating point: ${response.errorMessage}")
+ Toast.makeText(
+ this@AddBenchActivity,
+ "Erreur lors de la création du banc: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }) {
+ Text(stringResource(R.string.save))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Input(value: String, onValueChange: (String) -> Unit) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text("Description", color = Tildeeth) },
+ placeholder = { Text("Smash", color = TildeethAlpha) },
+ maxLines = 3,
+ textStyle = TextStyle(color = Tildeeth, fontFamily = Typography.bodyLarge.fontFamily),
+ modifier = Modifier.padding(8.dp).background(Lagoon).border(3.dp, Tildeeth)
+ )
+}
+
+@Composable
+fun Rating(value: Float, onValueChange: (Float) -> Unit) {
+ RatingBar(
+ value = value,
+ style = RatingBarStyle.Fill(),
+ onValueChange = onValueChange,
+ onRatingChanged = {
+ Log.d("TAG", "onRatingChanged: $it")
+ }
+ )
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt
new file mode 100644
index 0000000..5b77dcc
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/CustomAdapter.kt
@@ -0,0 +1,90 @@
+package io.trentetroim.benchmark
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+interface OnBenchClickListener {
+ fun onBenchClick(point: Point)
+}
+
+class CustomAdapter(
+ private val dataSet: List<Point>,
+ private val lifecycleScope: LifecycleCoroutineScope,
+ private val onBenchClickListener: OnBenchClickListener? = null
+) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
+
+ private val reviewRepository = ReviewRepository()
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val cardPfp: ImageView = view.findViewById(R.id.benchPfp)
+ val cardName: TextView
+ val cardPosition: TextView
+ val cardRating: RatingBar
+ val itemView: View = view
+
+ init {
+ cardName = view.findViewById(R.id.benchName)
+ cardPosition = view.findViewById(R.id.benchPos)
+ cardRating = view.findViewById(R.id.benchRating)
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(viewGroup.context)
+ .inflate(R.layout.card_layout, viewGroup, false)
+
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+ val point = dataSet[position]
+ viewHolder.cardName.text = point.name
+
+ val decimalFormat = java.text.DecimalFormat("0.000")
+ viewHolder.cardPosition.text = "Position: ${decimalFormat.format(point.x)}, ${decimalFormat.format(point.y)}"
+
+ // Fetch and display the average rating
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ viewHolder.cardRating.rating = response.data
+ }
+ else -> {
+ viewHolder.cardRating.rating = 0f
+ }
+ }
+ }
+ }
+
+ // Set click listener on the card view
+ viewHolder.itemView.setOnClickListener {
+ onBenchClickListener?.onBenchClick(point)
+ }
+
+ if (!point.picture.isNullOrEmpty()) {
+ Glide.with(viewHolder.itemView.context)
+ .load(point.picture)
+ .placeholder(R.drawable.ic_bench_icon)
+ .error(R.drawable.ic_bench_icon)
+ .into(viewHolder.cardPfp)
+ } else {
+ viewHolder.cardPfp.setImageResource(R.drawable.ic_bench_icon)
+ }
+ }
+
+ override fun getItemCount() = dataSet.size
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt
new file mode 100644
index 0000000..d99db2e
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/MainActivity.kt
@@ -0,0 +1,100 @@
+package io.trentetroim.benchmark
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.trentetroim.benchmark.ui.theme.BenchmarkTheme
+import android.content.Context
+import android.content.Intent
+import android.os.UserManager
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import io.trentetroim.benchmark.ui.theme.Lagoon
+import io.trentetroim.benchmark.ui.theme.Salmon
+import io.trentetroim.benchmark.ui.theme.White
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ installSplashScreen()
+ enableEdgeToEdge()
+ setContent {
+ BenchmarkTheme {
+ MainContent(Modifier.background(Lagoon))
+ }
+ }
+ }
+
+ @Composable
+ @Preview
+ fun MainContent(modifier : Modifier = Modifier) {
+ Column(
+ modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceEvenly
+ ) {
+ val user: UserManager = applicationContext.getSystemService(USER_SERVICE) as UserManager
+ Text("User is a goat: " + user.isUserAGoat().toString())
+ Infos()
+ IntentToSwitch(LocalContext.current)
+ Footer()
+ }
+ }
+
+ @Composable
+ @Preview
+ fun Infos() {
+ Image(
+ painter = painterResource(R.drawable.ic_bench_icon),
+ modifier = Modifier.size(200.dp),
+ contentDescription = "App icon",
+ contentScale = ContentScale.Fit
+ )
+ Text(text = stringResource(R.string.app_name), fontSize = 50.sp)
+ }
+
+ // composable with a button
+ @Composable
+ fun IntentToSwitch(context: Context) {
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, MapActivity::class.java))
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Salmon),
+ ) {
+ Text("Démarrer", color = White)
+ }
+ }
+
+ @Composable
+ @Preview
+ fun Footer() {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = stringResource(R.string.devs),
+ )
+ Text(stringResource(R.string.quote), fontStyle = FontStyle.Italic, fontFamily = FontFamily.Cursive)
+ }
+ }
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt
new file mode 100644
index 0000000..09376b2
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/MapActivity.kt
@@ -0,0 +1,811 @@
+package io.trentetroim.benchmark
+
+import android.Manifest
+import android.animation.ValueAnimator
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.preference.PreferenceManager.getDefaultSharedPreferences
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.RatingBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.app.ActivityCompat
+import androidx.core.animation.doOnEnd
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationServices
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.repository.PointRepository
+import io.trentetroim.benchmark.api.repository.ReviewRepository
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.osmdroid.config.Configuration.getInstance
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.util.GeoPoint
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.ItemizedIconOverlay
+import org.osmdroid.views.overlay.ItemizedOverlayWithFocus
+import org.osmdroid.views.overlay.OverlayItem
+import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
+import org.osmdroid.views.overlay.MapEventsOverlay
+import org.osmdroid.events.MapEventsReceiver
+import androidx.core.graphics.drawable.toDrawable
+import org.osmdroid.bonuspack.routing.OSRMRoadManager
+import org.osmdroid.bonuspack.routing.Road
+import org.osmdroid.bonuspack.routing.RoadManager
+import org.osmdroid.views.overlay.Polyline
+
+class MapActivity : ComponentActivity(), MapEventsReceiver, OnBenchClickListener {
+ private lateinit var map: MapView
+
+ private lateinit var locationClient: FusedLocationProviderClient
+ private val locationPermissionRequest = 1001
+ private var latitude = 46.0837
+ private var longitude = 6.0452
+
+ private val pointRepository = PointRepository()
+ private val reviewRepository = ReviewRepository()
+ private var points: List<Point> = emptyList()
+ private val TAG = "MapActivity"
+ private lateinit var bottomText: TextView;
+ private lateinit var listContainer: CoordinatorLayout;
+ private lateinit var sheetBehavior: BottomSheetBehavior<LinearLayout>
+
+
+ private var isDialogShowing = false
+
+ override fun onBenchClick(point: Point) {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = GeoPoint(point.x, point.y)
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 17.5)
+ endZoom = 17.5
+
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.start()
+ }
+
+ override fun singleTapConfirmedHelper(p0: GeoPoint): Boolean {
+ return false
+ }
+
+ override fun longPressHelper(p0: GeoPoint): Boolean {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = p0
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 17.5)
+ endZoom = 17.5
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.doOnEnd {
+ Thread.sleep(400)
+ showAddBenchDialog(p0.latitude, p0.longitude)
+ }
+ animator.start()
+ return true
+ }
+
+ private fun showAddBenchDialog(lat: Double, lon: Double) {
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", lat)
+ intent.putExtra("currentLongitude", lon)
+ startActivity(intent)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // enableEdgeToEdge()
+
+ getInstance().load(this, getDefaultSharedPreferences(this))
+
+ setContentView(R.layout.main_layout)
+ sheetBehavior = BottomSheetBehavior.from<LinearLayout>(findViewById<LinearLayout>(R.id.slideUpContainer))
+
+ sheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ when (newState) {
+ BottomSheetBehavior.STATE_HIDDEN -> {
+ bottomText.text = "Steak haché"
+ }
+
+ BottomSheetBehavior.STATE_EXPANDED ->
+ bottomText.text = "Bas!"
+
+ BottomSheetBehavior.STATE_COLLAPSED ->
+ bottomText.text = "Haut!"
+
+ BottomSheetBehavior.STATE_DRAGGING -> {
+ bottomText.text = "Oh oui tire plus fort"
+ }
+
+ BottomSheetBehavior.STATE_SETTLING -> {
+ bottomText.text = "je câble"
+ }
+
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> {
+ TODO()
+ }
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ bottomText.text = "Oh oui maître tirez encore " + ((1 - slideOffset) * 100) + "% de plus"
+ }
+ })
+
+
+ map = findViewById(R.id.map)
+ map.setTileSource(TileSourceFactory.MAPNIK)
+
+ val mapController = map.controller
+ mapController.setZoom(15.0)
+ val startPoint = GeoPoint(latitude, longitude)
+ mapController.setCenter(startPoint)
+
+ val rotationGestureOverlay = RotationGestureOverlay(map)
+ rotationGestureOverlay.isEnabled
+ map.setMultiTouchControls(true)
+ map.overlays.add(rotationGestureOverlay)
+
+ val mapEventsOverlay = MapEventsOverlay(this)
+ map.overlays.add(mapEventsOverlay)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.greenwashed)) { v, windowInsets ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+ leftMargin = insets.left
+ bottomMargin = insets.bottom
+ rightMargin = insets.right
+ }
+
+ WindowInsetsCompat.CONSUMED
+ }
+
+ val newBenchBtn: ImageButton = findViewById(R.id.plusBtn)
+ locationClient = LocationServices.getFusedLocationProviderClient(this)
+ newBenchBtn.setOnClickListener {
+ val center = map.mapCenter as GeoPoint
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", center.latitude)
+ intent.putExtra("currentLongitude", center.longitude)
+ startActivity(intent)
+ }
+ val centerPosBtn: ImageButton = findViewById(R.id.posBtn)
+ centerPosBtn.setOnClickListener {
+ sendCurrentLocation(false)
+ }
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+
+ bottomText = findViewById(R.id.divider)
+ listContainer = findViewById(R.id.listWrapper)
+
+ sendCurrentLocation(false)
+ }
+
+ private fun sendCurrentLocation(launchDialog: Boolean = true) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ locationPermissionRequest
+ )
+ return
+ }
+ locationClient.lastLocation.addOnSuccessListener { location ->
+ if (location != null) {
+ latitude = location.latitude
+ longitude = location.longitude
+
+ if (!launchDialog) {
+ val startPoint = map.mapCenter as GeoPoint
+ val endPoint = GeoPoint(location.latitude, location.longitude)
+ val startZoom = map.zoomLevelDouble
+ var endZoom = map.zoomLevelDouble
+ if (endZoom < 18.0)
+ endZoom = 18.0
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = 1000
+ animator.addUpdateListener { animation ->
+ val fraction = animation.animatedFraction
+ val lat = startPoint.latitude + (endPoint.latitude - startPoint.latitude) * fraction
+ val lon = startPoint.longitude + (endPoint.longitude - startPoint.longitude) * fraction
+ val zoom = startZoom + (endZoom - startZoom) * fraction
+ map.controller.setCenter(GeoPoint(lat, lon))
+ map.controller.setZoom(zoom)
+ }
+ animator.start()
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ fetchPoints(recyclerView)
+ } else {
+ val intent = Intent(baseContext, AddBenchActivity::class.java)
+ intent.putExtra("currentLatitude", location.latitude)
+ intent.putExtra("currentLongitude", location.longitude)
+ startActivity(intent)
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "Merci d'activer la localisation",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array<String>,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ if (requestCode == locationPermissionRequest &&
+ grantResults.isNotEmpty() &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
+ ) {
+ sendCurrentLocation()
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "La permission d'accès à la position est requise pour ajouter un banc",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
+ val r = 6371
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+ val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2)
+ val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+ return r * c
+ }
+
+ private fun fetchPoints(recyclerView: RecyclerView) {
+ lifecycleScope.launch {
+ pointRepository.getPoints().collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ bottomText.text = "Chargement..."
+ }
+
+ is ApiResponse.Success -> {
+ points = response.data.sortedBy { point ->
+ calculateDistance(latitude, longitude, point.x, point.y)
+ }
+
+ val customAdapter = CustomAdapter(points, lifecycleScope, this@MapActivity)
+ recyclerView.adapter = customAdapter
+
+ addMarkersToMap()
+ bottomText.text = "Up!"
+ }
+
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Error loading points: ${response.errorMessage}",
+ Toast.LENGTH_LONG
+ ).show()
+
+ /* val fallbackPoints = listOf(
+ Point(id = "1", x = 48.8566, y = 2.3522, type = "bench", name = "Bench 1"),
+ Point(id = "2", x = 48.8566, y = 2.3523, type = "bench", name = "Bench 2"),
+ Point(id = "3", x = 48.8566, y = 2.3524, type = "bench", name = "Bench 3"),
+ Point(id = "4", x = 48.8566, y = 2.3525, type = "bench", name = "Bench 4"),
+ Point(id = "5", x = 48.8566, y = 2.3526, type = "bench", name = "Bench 5")
+ )
+
+ points = fallbackPoints
+ val customAdapter = CustomAdapter(fallbackPoints, lifecycleScope, this@MapActivity)
+ recyclerView.adapter = customAdapter
+
+ addMarkersToMap()*/
+ bottomText.text = "Erreur bozo"
+ }
+ }
+ }
+ }
+ }
+
+ private fun addMarkersToMap() {
+ while (map.overlays.size > 2) {
+ map.overlays.removeAt(map.overlays.size - 1)
+ }
+
+ val items = ArrayList<OverlayItem>()
+ val decimalFormat = java.text.DecimalFormat("0.00")
+ val decimalFormatPopup = java.text.DecimalFormat("0.000")
+
+ for (point in points) {
+ val item = OverlayItem(
+ point.name,
+ "Type: ${point.type} | Position: ${decimalFormat.format(point.x)}, ${decimalFormat.format(point.y)}",
+ GeoPoint(point.x, point.y)
+ )
+ items.add(item)
+ }
+
+ if (items.isNotEmpty()) {
+ val overlay = ItemizedOverlayWithFocus(
+ items,
+ object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
+ override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
+ val point = points.getOrNull(index)
+ if (point != null) {
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ val rating = response.data
+ if (!point.picture.isNullOrEmpty()) {
+ showBenchPictureDialog(
+ point, item, rating,
+ decimalFormatPopup.format(point.x),
+ decimalFormatPopup.format(point.y)
+ )
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\nType: ${point.type} | Position: ${
+ decimalFormatPopup.format(
+ point.x
+ )
+ }, ${decimalFormatPopup.format(point.y)}\nRating: $rating/5",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ else -> {
+ if (!point.picture.isNullOrEmpty()) {
+ showBenchPictureDialog(
+ point, item, 0f,
+ decimalFormatPopup.format(point.x),
+ decimalFormatPopup.format(point.y)
+ )
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\nType: ${point.type} | Position: ${
+ decimalFormatPopup.format(
+ point.x
+ )
+ }, ${decimalFormatPopup.format(point.y)}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "${item.title}\n${item.snippet}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ return true
+ }
+
+ override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
+ return false
+ }
+ },
+ this
+ )
+
+ overlay.setFocusItemsOnTap(false)
+ map.overlays.add(overlay)
+ map.invalidate()
+ }
+ }
+
+ private fun loadMarkerImage(point: Point, item: OverlayItem) {
+ try {
+ Log.d(TAG, "Attempting to fetch image for marker: ${point.picture}")
+
+ Glide.with(this)
+ .asBitmap()
+ .load(point.picture)
+ .into(object : CustomTarget<Bitmap>(100, 100) {
+ override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
+ val drawable = resource.toDrawable(resources)
+ item.setMarker(drawable)
+ map.invalidate()
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ Log.e(TAG, "Echec de chargement de l'image du point ${point.id}")
+ }
+ })
+ } catch (e: Exception) {
+ Log.e(TAG, "Echec de chargement du marqueur: ${e.message}")
+ }
+ }
+
+ private fun showBenchPictureDialog(point: Point, item: OverlayItem, rating: Float, xTrunc: String, yTrunc: String) {
+ if (isDialogShowing) {
+ Log.d(TAG, "Dialogue déja ouvert!")
+ return
+ }
+
+ isDialogShowing = true
+
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_bench_details, null)
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setView(dialogView)
+ .setOnDismissListener {
+ isDialogShowing = false
+ }
+ .create()
+
+ val titleTextView = dialogView.findViewById<TextView>(R.id.dialogTitle)
+ val messageTextView = dialogView.findViewById<TextView>(R.id.dialogMessage)
+ val benchImageView = dialogView.findViewById<ImageView>(R.id.benchImageView)
+
+ titleTextView.text = item.title
+
+ messageTextView.text = "Chargement des informations..."
+
+ Glide.with(this)
+ .load(point.picture)
+ .placeholder(R.drawable.ic_bench_icon)
+ .error(R.drawable.ic_bench_icon)
+ .into(benchImageView)
+
+ dialogView.findViewById<Button>(R.id.btnAddReview).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ openReviewDialog(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnViewReviews).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ showReviewsDialog(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnShowPath).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ showPathToBench(point)
+ }
+
+ dialogView.findViewById<Button>(R.id.btnClose).setOnClickListener {
+ dialog.dismiss()
+ isDialogShowing = false
+ }
+
+ dialog.show()
+
+ lifecycleScope.launch {
+ reviewRepository.getAverageRating(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Success -> {
+ val avgRating = response.data
+ messageTextView.text = "Type: ${point.type} | Position: $xTrunc, $yTrunc\nNote moyenne: ${String.format("%.1f", avgRating)}/5"
+ }
+ is ApiResponse.Error -> {
+ messageTextView.text = "Type: ${point.type} | Position: $xTrunc, $yTrunc\nRating: $rating/5"
+ Log.e(TAG, "Error fetching average rating: ${response.errorMessage}")
+ }
+ is ApiResponse.Loading -> {
+ }
+ }
+ }
+ }
+ }
+
+ private fun openReviewDialog(point: Point) {
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_review, null)
+ val ratingBar = dialogView.findViewById<RatingBar>(R.id.ratingBar)
+ val commentEditText = dialogView.findViewById<EditText>(R.id.commentEditText)
+
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setTitle("Ajouter un avis pour ${point.name}")
+ .setView(dialogView)
+ .setPositiveButton("Soumettre", null)
+ .setNegativeButton("Annuler") { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+
+ dialog.setOnShowListener {
+ val positiveButton = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
+ positiveButton.setOnClickListener {
+ val rating = ratingBar.rating.toInt()
+ val comment = commentEditText.text.toString()
+
+ if (rating == 0) {
+ Toast.makeText(this, "Veuillez donner une note", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
+ val review = Review(
+ id = "",
+ pointId = point.id,
+ grade = rating,
+ comment = comment.ifEmpty { null },
+ pictures = null
+ )
+
+ submitReview(point.id, review)
+ dialog.dismiss()
+ }
+ }
+
+ dialog.show()
+ }
+
+ private fun submitReview(pointId: String, review: Review) {
+ val reviewRepository = ReviewRepository()
+
+ lifecycleScope.launch {
+ reviewRepository.addReview(pointId, review).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ }
+ is ApiResponse.Success -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Avis soumis avec succès!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+ private fun showPathToBench(point: Point) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ locationPermissionRequest
+ )
+ Toast.makeText(
+ this,
+ "Permission de localisation nécessaire pour afficher l'itinéraire",
+ Toast.LENGTH_LONG
+ ).show()
+ return
+ }
+
+ Toast.makeText(
+ this,
+ "Calcul de l'itinéraire en cours...",
+ Toast.LENGTH_SHORT
+ ).show()
+
+ locationClient.lastLocation.addOnSuccessListener { location ->
+ if (location != null) {
+ val roadManager = OSRMRoadManager(this, "OsmDroid/6.1.0")
+ roadManager.setMean(OSRMRoadManager.MEAN_BY_FOOT)
+ lifecycleScope.launch {
+ try {
+ for (overlay in map.overlays) {
+ if (overlay is Polyline) {
+ map.overlays.remove(overlay)
+ }
+ }
+
+ val waypoints = ArrayList<GeoPoint>()
+ waypoints.add(GeoPoint(location.latitude, location.longitude))
+ waypoints.add(GeoPoint(point.x, point.y))
+
+ val road = roadManager.getRoad(waypoints)
+
+ val roadOverlay = RoadManager.buildRoadOverlay(road)
+ roadOverlay.outlinePaint.color = android.graphics.Color.BLUE
+ roadOverlay.outlinePaint.strokeWidth = 10f
+
+ map.overlays.add(roadOverlay)
+
+ val bounds = road.mBoundingBox
+ map.zoomToBoundingBox(bounds, true, 100)
+
+ map.invalidate()
+
+ val distance = road.mLength
+ val duration = road.mDuration / 60
+
+ Toast.makeText(
+ this@MapActivity,
+ "Distance: ${String.format("%.2f", distance)} km\nDurée: ${String.format("%.0f", duration)} min",
+ Toast.LENGTH_LONG
+ ).show()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error calculating route: ${e.message}")
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur lors du calcul de l'itinéraire: ${e.message}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ } else {
+ Toast.makeText(
+ this@MapActivity,
+ "Impossible d'obtenir votre position actuelle",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ private fun showReviewsDialog(point: Point) {
+ val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_reviews_list, null)
+ val recyclerView = dialogView.findViewById<RecyclerView>(R.id.reviewsRecyclerView)
+ val averageRatingText = dialogView.findViewById<TextView>(R.id.averageRatingText)
+ val noReviewsText = dialogView.findViewById<TextView>(R.id.noReviewsText)
+ val reviewsTitle = dialogView.findViewById<TextView>(R.id.reviewsTitle)
+
+ reviewsTitle.text = "Avis pour ${point.name}"
+
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ val adapter = ReviewAdapter(emptyList())
+ recyclerView.adapter = adapter
+
+ val dialog = android.app.AlertDialog.Builder(this)
+ .setView(dialogView)
+ .setPositiveButton("Fermer", null)
+ .create()
+
+ dialog.show()
+
+ lifecycleScope.launch {
+ reviewRepository.getReviews(point.id).collectLatest { response ->
+ when (response) {
+ is ApiResponse.Loading -> {
+ }
+ is ApiResponse.Success -> {
+ val reviews = response.data
+ if (reviews.isEmpty()) {
+ recyclerView.visibility = View.GONE
+ noReviewsText.visibility = View.VISIBLE
+ } else {
+ recyclerView.visibility = View.VISIBLE
+ noReviewsText.visibility = View.GONE
+ adapter.updateReviews(reviews)
+ }
+
+ reviewRepository.getAverageRating(point.id).collectLatest { ratingResponse ->
+ when (ratingResponse) {
+ is ApiResponse.Success -> {
+ val avgRating = ratingResponse.data
+ averageRatingText.text = "Note moyenne: ${String.format("%.1f", avgRating)}/5"
+ }
+ else -> {
+ }
+ }
+ }
+ }
+ is ApiResponse.Error -> {
+ Toast.makeText(
+ this@MapActivity,
+ "Erreur: ${response.errorMessage}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+
+ override fun onResume() {
+ super.onResume()
+ map.onResume()
+
+ val recyclerView: RecyclerView = findViewById(R.id.greenwashed)
+ fetchPoints(recyclerView)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ map.onPause()
+ }
+
+ /*override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ val permissionsToRequest = ArrayList<String>()
+ var i = 0
+ while (i < grantResults.size) {
+ permissionsToRequest.add(permissions[i])
+ i++
+ }
+ if (permissionsToRequest.size > 0) {
+ ActivityCompat.requestPermissions(
+ this,
+ permissionsToRequest.toTypedArray(),
+ REQUEST_PERMISSIONS_REQUEST_CODE)
+ }
+ }*/
+
+
+ /*private fun requestPermissionsIfNecessary(String[] permissions) {
+ val permissionsToRequest = ArrayList<String>();
+ permissions.forEach { permission ->
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ permissionsToRequest.add(permission);
+ }
+ }
+ if (permissionsToRequest.size() > 0) {
+ ActivityCompat.requestPermissions(
+ this,
+ permissionsToRequest.toArray(new String[0]),
+ REQUEST_PERMISSIONS_REQUEST_CODE);
+ }
+ }*/
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt
new file mode 100644
index 0000000..2dffb3a
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ReviewAdapter.kt
@@ -0,0 +1,37 @@
+package io.trentetroim.benchmark
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RatingBar
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import io.trentetroim.benchmark.api.models.Review
+
+class ReviewAdapter(private var reviews: List<Review>) :
+ RecyclerView.Adapter<ReviewAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val ratingBar: RatingBar = view.findViewById(R.id.itemRatingBar)
+ val commentText: TextView = view.findViewById(R.id.itemCommentText)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_review, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val review = reviews[position]
+ holder.ratingBar.rating = review.grade.toFloat()
+ holder.commentText.text = review.comment ?: "Pas de commentaire"
+ }
+
+ override fun getItemCount() = reviews.size
+
+ fun updateReviews(newReviews: List<Review>) {
+ reviews = newReviews
+ notifyDataSetChanged()
+ }
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt
new file mode 100644
index 0000000..a3dcd56
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/ApiResponse.kt
@@ -0,0 +1,16 @@
+package io.trentetroim.benchmark.api.models
+
+sealed class ApiResponse<out T> {
+ data class Success<out T>(val data: T) : ApiResponse<T>()
+ data class Error(val errorMessage: String, val code: Int? = null) : ApiResponse<Nothing>()
+ object Loading : ApiResponse<Nothing>()
+}
+
+
+data class PointsResponse(
+ val points: List<Point>
+)
+
+data class ReviewsResponse(
+ val reviews: List<Review>
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt
new file mode 100644
index 0000000..123300f
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Point.kt
@@ -0,0 +1,24 @@
+package io.trentetroim.benchmark.api.models
+
+import com.google.gson.annotations.SerializedName
+
+
+data class Point(
+ @SerializedName("id")
+ val id: String,
+
+ @SerializedName("x")
+ val x: Double,
+
+ @SerializedName("y")
+ val y: Double,
+
+ @SerializedName("type")
+ val type: String,
+
+ @SerializedName("name")
+ val name: String,
+
+ @SerializedName("picture")
+ val picture: String? = null,
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt
new file mode 100644
index 0000000..6539608
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/models/Review.kt
@@ -0,0 +1,20 @@
+package io.trentetroim.benchmark.api.models
+
+import com.google.gson.annotations.SerializedName
+
+data class Review(
+ @SerializedName("id")
+ val id: String,
+
+ @SerializedName("pointId")
+ val pointId: String,
+
+ @SerializedName("grade")
+ val grade: Int,
+
+ @SerializedName("comment")
+ val comment: String? = null,
+
+ @SerializedName("pictures")
+ val pictures: List<String>? = null
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt
new file mode 100644
index 0000000..ad404a0
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/PointRepository.kt
@@ -0,0 +1,164 @@
+package io.trentetroim.benchmark.api.repository
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.service.RetrofitClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import java.io.FileOutputStream
+
+class PointRepository {
+ private val apiService = RetrofitClient.apiService
+
+ fun getPoints(type: String? = null): Flow<ApiResponse<List<Point>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getPoints(type)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun getPointById(id: String): Flow<ApiResponse<Point>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getPointById(id)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+
+ fun getReviews(pointId: String): Flow<ApiResponse<List<Review>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun addReview(pointId: String, review: Review): Flow<ApiResponse<Review>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.addReview(pointId, review)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun createPoint(point: Point): Flow<ApiResponse<Point>> = flow {
+ emit(ApiResponse.Loading)
+
+ Log.d("PointRepository", "Creating point: $point")
+
+ try {
+ val response = apiService.createPoint(point)
+
+ Log.d("PointRepository", "Response: ${response.code()} ${response.message()}")
+
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun uploadImage(context: Context, imageUri: Uri): Flow<ApiResponse<String>> = flow {
+ emit(ApiResponse.Loading)
+
+ try {
+ val file = createTempFileFromUri(context, imageUri)
+
+ if (file == null) {
+ emit(ApiResponse.Error("Failed to create file from URI"))
+ return@flow
+ }
+
+ if (file.length() > 8 * 1024 * 1024) {
+ emit(ApiResponse.Error("File size exceeds 8MB limit"))
+ return@flow
+ }
+ val requestBody = file.asRequestBody("image/*".toMediaTypeOrNull())
+ val part = MultipartBody.Part.createFormData("picture", file.name, requestBody)
+
+ val response = apiService.uploadImage(part)
+
+ if (response.isSuccessful) {
+ response.body()?.let { responseMap ->
+ val url = responseMap["url"]
+ if (url != null) {
+ emit(ApiResponse.Success(url))
+ } else {
+ emit(ApiResponse.Error("URL not found in response"))
+ }
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ private fun createTempFileFromUri(context: Context, uri: Uri): File? {
+ return try {
+ val inputStream = context.contentResolver.openInputStream(uri)
+ val tempFile = File.createTempFile("upload", ".jpg", context.cacheDir)
+
+ inputStream?.use { input ->
+ FileOutputStream(tempFile).use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ tempFile
+ } catch (e: Exception) {
+ Log.e("PointRepository", "Error creating temp file: ${e.message}")
+ null
+ }
+ }
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt
new file mode 100644
index 0000000..4636239
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/repository/ReviewRepository.kt
@@ -0,0 +1,69 @@
+package io.trentetroim.benchmark.api.repository
+
+import android.util.Log
+import io.trentetroim.benchmark.api.models.ApiResponse
+import io.trentetroim.benchmark.api.models.Review
+import io.trentetroim.benchmark.api.service.RetrofitClient
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class ReviewRepository {
+ private val apiService = RetrofitClient.apiService
+ private val TAG = "ReviewRepository"
+
+ fun getReviews(pointId: String): Flow<ApiResponse<List<Review>>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun addReview(pointId: String, review: Review): Flow<ApiResponse<Review>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.addReview(pointId, review)
+ if (response.isSuccessful) {
+ response.body()?.let {
+ emit(ApiResponse.Success(it))
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ fun getAverageRating(pointId: String): Flow<ApiResponse<Float>> = flow {
+ emit(ApiResponse.Loading)
+ try {
+ val response = apiService.getReviews(pointId)
+ if (response.isSuccessful) {
+ response.body()?.let { reviews ->
+ if (reviews.isEmpty()) {
+ emit(ApiResponse.Success(0f))
+ } else {
+ val averageRating = reviews.map { it.grade }.average().toFloat()
+ Log.d(TAG, "Average rating for point $pointId: $averageRating")
+ emit(ApiResponse.Success(averageRating))
+ }
+ } ?: emit(ApiResponse.Error("Empty response body"))
+ } else {
+ emit(ApiResponse.Error("Error: ${response.code()} ${response.message()}"))
+ }
+ } catch (e: Exception) {
+ emit(ApiResponse.Error(e.message ?: "Unknown error"))
+ }
+ }.flowOn(Dispatchers.IO)
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt
new file mode 100644
index 0000000..91e8c24
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/ApiService.kt
@@ -0,0 +1,38 @@
+package io.trentetroim.benchmark.api.service
+
+import io.trentetroim.benchmark.api.models.Point
+import io.trentetroim.benchmark.api.models.Review
+import okhttp3.MultipartBody
+import retrofit2.Response
+import retrofit2.http.*
+
+interface ApiService {
+
+ @GET("/api/points")
+ suspend fun getPoints(@Query("type") type: String? = null): Response<List<Point>>
+
+
+ @GET("/api/points/{id}")
+ suspend fun getPointById(@Path("id") id: String): Response<Point>
+
+ @POST("/api/points")
+ suspend fun createPoint(@Body point: Point): Response<Point>
+
+
+ @GET("/api/points/{id}/reviews")
+ suspend fun getReviews(@Path("id") pointId: String): Response<List<Review>>
+
+
+ @POST("/api/points/{id}/reviews")
+ suspend fun addReview(
+ @Path("id") pointId: String,
+ @Body review: Review
+ ): Response<Review>
+
+ @Multipart
+ @POST("/api/upload")
+ suspend fun uploadImage(
+ @Part image: MultipartBody.Part
+ ): Response<Map<String, String>>
+
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt
new file mode 100644
index 0000000..7185d3c
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/api/service/RetrofitClient.kt
@@ -0,0 +1,43 @@
+package io.trentetroim.benchmark.api.service
+
+import com.google.gson.GsonBuilder
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+
+object RetrofitClient {
+ private const val BASE_URL = "http://89.168.39.144:5431"
+ private const val TIMEOUT = 30L
+
+ private fun createOkHttpClient(): OkHttpClient {
+ val loggingInterceptor = HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+ return OkHttpClient.Builder()
+ .addInterceptor(loggingInterceptor)
+ .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .build()
+ }
+
+ private val retrofit: Retrofit by lazy {
+ val gson = GsonBuilder()
+ .setLenient()
+ .create()
+
+ Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(createOkHttpClient())
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .build()
+ }
+
+ val apiService: ApiService by lazy {
+ retrofit.create(ApiService::class.java)
+ }
+}
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt
new file mode 100644
index 0000000..d85d9b7
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Color.kt
@@ -0,0 +1,17 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val White = Color(0xFFFFFFFF)
+val Pink40 = Color(0xFF7D5260)
+val Salmon = Color(0xFF8C69FF)
+val Grass = Color(0xff81c9b5)
+val Lagoon = Color(0xff90d8c4)
+val Tildeeth = Color(0xff99004d)
+val TildeethAlpha = Color(0x7799004d) \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt
new file mode 100644
index 0000000..cf2034d
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Theme.kt
@@ -0,0 +1,55 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80,
+ background = Grass
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40,
+ background = Lagoon,
+
+// Other default colors to override
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+)
+
+@Composable
+fun BenchmarkTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
+ context
+ )
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+} \ No newline at end of file
diff --git a/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt
new file mode 100644
index 0000000..b731516
--- /dev/null
+++ b/benchmark/app/src/main/java/io/trentetroim/benchmark/ui/theme/Type.kt
@@ -0,0 +1,36 @@
+package io.trentetroim.benchmark.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import io.trentetroim.benchmark.R
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily(Font(resId = R.font.nokiafc22, weight = FontWeight.Normal)),
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ */
+ labelLarge = TextStyle(
+ fontFamily = FontFamily(Font(resId = R.font.nokiafc22, weight = FontWeight.Medium)),
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+) \ No newline at end of file
diff --git a/benchmark/app/src/main/res/drawable/card_bg.xml b/benchmark/app/src/main/res/drawable/card_bg.xml
new file mode 100644
index 0000000..6c35bba
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/card_bg.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:type="linear"
+ android:angle="180"
+ android:startColor="@color/lagoon"
+ android:endColor="#dcf3ec"/>
+<!--
+ <stroke android:width="1dp" android:color="#B1BCBE"/>-->
+ <corners
+ android:radius="10dp"/>
+</shape>
diff --git a/benchmark/app/src/main/res/drawable/ic_bench_icon.xml b/benchmark/app/src/main/res/drawable/ic_bench_icon.xml
new file mode 100644
index 0000000..a10538e
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/ic_bench_icon.xml
@@ -0,0 +1,93 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="192dp"
+ android:height="192dp"
+ android:viewportWidth="512"
+ android:viewportHeight="512">
+ <path
+ android:pathData="M15.019 195.698L169.247 106.655 207.804 128.915 207.804 255.999 53.577 345.044 15.019 322.782Z"
+ android:fillColor="#ACABB1"/>
+ <path
+ android:pathData="M15.019 195.698L15.019 322.782 53.577 345.044 53.577 217.96Z"
+ android:fillColor="#C6C5CB"/>
+ <path
+ android:pathData="M15.019 195.698L169.247 106.655 207.804 128.915 53.577 217.96Z"
+ android:fillColor="#D8D8DA"/>
+ <path
+ android:pathData="M15.019 166.956L169.247 77.913 207.804 100.174 207.804 122.434 53.577 211.478 15.019 189.217Z"
+ android:fillColor="#FFA834"/>
+ <path
+ android:pathData="M15.019 189.217L15.019 166.956 53.577 189.217 53.577 211.478Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M53.577 189.217L207.804 100.174 207.804 122.434 53.577 211.478Z"
+ android:fillColor="#D07400"/>
+ <path
+ android:pathData="M53.577 233.739L111.412 200.348 362.003 345.041 304.196 378.435 304.196 400.695 53.577 255.999Z"
+ android:fillColor="#FF9911"/>
+ <path
+ android:pathData="M304.196 378.435L53.577 233.739 53.577 255.999 304.196 400.695Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M72.855 267.13L72.855 298.689 304.238 432.288 304.196 400.695Z"
+ android:fillColor="#D07400"/>
+ <path
+ android:pathData="M130.69 189.217L130.689 211.478 362.003 345.041 419.867 311.652 169.247 166.956Z"
+ android:fillColor="#FF9911"/>
+ <path
+ android:pathData="M130.69 189.217L130.689 211.478 362.003 345.041 381.311 333.913Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M208.838 11.131L207.804 77.913 458.424 222.608 477.702 211.478 477.702 144.696 227.083 0Z"
+ android:fillColor="#FF9911"/>
+ <path
+ android:pathData="M207.804 100.174L207.804 144.696 458.424 289.391 477.702 278.261 477.702 233.739 227.083 89.043Z"
+ android:fillColor="#FF9911"/>
+ <path
+ android:pathData="M208.838 11.131L227.083 0 477.702 144.696 458.424 155.826Z"
+ android:fillColor="#FFA834"/>
+ <path
+ android:pathData="M207.804 100.174L227.083 89.043 477.702 233.739 458.424 244.87Z"
+ android:fillColor="#FFA834"/>
+ <path
+ android:pathData="M477.702 144.696L477.702 211.478 458.424 222.608 458.424 155.826Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M477.702 233.739L477.702 278.261 458.424 289.391 458.424 244.87Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M304.196 362.654L458.424 273.611 496.981 295.872 496.981 422.956 342.753 512 304.196 489.738Z"
+ android:fillColor="#ACABB1"/>
+ <path
+ android:pathData="M304.196 362.654L304.196 489.738 342.753 512 342.753 384.916Z"
+ android:fillColor="#C6C5CB"/>
+ <path
+ android:pathData="M304.196 362.654L458.424 273.611 496.981 295.872 342.753 384.916Z"
+ android:fillColor="#D8D8DA"/>
+ <path
+ android:pathData="M304.196 333.913L458.424 244.87 496.981 267.13 496.981 289.391 342.753 378.435 304.196 356.173Z"
+ android:fillColor="#FFA834"/>
+ <path
+ android:pathData="M304.196 356.173L304.196 333.913 342.753 356.173 342.753 378.435Z"
+ android:fillColor="#F18700"/>
+ <path
+ android:pathData="M342.753 356.173L496.981 267.13 496.981 289.391 342.753 378.435Z"
+ android:fillColor="#D07400"/>
+ <path
+ android:pathData="M284.928 189.211L246.372 166.951 246.372 189.187 227.083 200.348 265.65 222.603 284.928 211.448Z"
+ android:fillColor="#ACABB1"/>
+ <path
+ android:pathData="M246.372 166.951L246.372 189.187 265.702 200.348 265.702 178.08Z"
+ android:fillColor="#C6C5CB"/>
+ <path
+ android:pathData="M246.372 189.187c0 0 -19.299 11.167 -19.288 11.161c0.01 -0.006 19.374 11.076 19.331 11.1s19.288 -11.1 19.288 -11.1L246.372 189.187z"
+ android:fillColor="#D8D8DA"/>
+ <path
+ android:pathData="M400.546 256.024L361.99 233.764 361.99 255.999 342.701 267.161 381.268 289.415 400.546 278.261Z"
+ android:fillColor="#ACABB1"/>
+ <path
+ android:pathData="M361.99 233.764L361.99 255.999 381.32 267.161 381.32 244.893Z"
+ android:fillColor="#C6C5CB"/>
+ <path
+ android:pathData="M361.99 255.999c0 0 -19.299 11.167 -19.288 11.161c0.01 -0.006 19.374 11.076 19.331 11.1c-0.043 0.025 19.288 -11.1 19.288 -11.1L361.99 255.999z"
+ android:fillColor="#D8D8DA"/>
+</vector> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/drawable/ic_launcher_background.xml b/benchmark/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..956b344
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="108dp"
+ android:width="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/benchmark/app/src/main/res/drawable/ic_launcher_foreground.xml b/benchmark/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1ee1493
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startY="49.59793"
+ android:startX="42.9492"
+ android:endY="92.4963"
+ android:endX="85.84757"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0"/>
+ <item
+ android:color="#00000000"
+ android:offset="1.0"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000"/>
+</vector> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/drawable/list_handle_layout.xml b/benchmark/app/src/main/res/drawable/list_handle_layout.xml
new file mode 100644
index 0000000..3470014
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/list_handle_layout.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:type="linear"
+ android:angle="270"
+ android:startColor="@color/salmon"
+ android:endColor="#ffdbd0"/>
+
+ <stroke android:width="3dp" android:color="#B1BCBE"/>
+ <corners
+ android:radius="10dp"/>
+</shape>
diff --git a/benchmark/app/src/main/res/drawable/plus_5345954.png b/benchmark/app/src/main/res/drawable/plus_5345954.png
new file mode 100644
index 0000000..257843f
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/plus_5345954.png
Binary files differ
diff --git a/benchmark/app/src/main/res/drawable/target.xml b/benchmark/app/src/main/res/drawable/target.xml
new file mode 100644
index 0000000..f22fb1a
--- /dev/null
+++ b/benchmark/app/src/main/res/drawable/target.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
+ android:viewportWidth="512"
+ android:viewportHeight="512"
+ android:width="50dp"
+ android:height="50dp">
+ <path
+ android:pathData="M256 176c-44.004 0 -80.001 36 -80.001 80 0 44.004 35.997 80 80.001 80 44.005 0 79.999 -35.996 79.999 -80 0 -44 -35.994 -80 -79.999 -80zm190.938 58.667c-9.605 -88.531 -81.074 -160 -169.605 -169.599V32h-42.666v33.067c-88.531 9.599 -160 81.068 -169.604 169.599H32v42.667h33.062c9.604 88.531 81.072 160 169.604 169.604V480h42.666v-33.062c88.531 -9.604 160 -81.073 169.605 -169.604H480v-42.667h-33.062zM256 405.333c-82.137 0 -149.334 -67.198 -149.334 -149.333 0 -82.136 67.197 -149.333 149.334 -149.333 82.135 0 149.332 67.198 149.332 149.333S338.135 405.333 256 405.333z"
+ android:fillColor="#000000" />
+</vector> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/font/nokia_font.xml b/benchmark/app/src/main/res/font/nokia_font.xml
new file mode 100644
index 0000000..136a952
--- /dev/null
+++ b/benchmark/app/src/main/res/font/nokia_font.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android">
+ <font
+ android:font="@font/nokiafc22"
+ android:fontStyle="normal"
+ android:fontWeight="800"/>
+</font-family> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/font/nokiafc22.ttf b/benchmark/app/src/main/res/font/nokiafc22.ttf
new file mode 100644
index 0000000..9dd011a
--- /dev/null
+++ b/benchmark/app/src/main/res/font/nokiafc22.ttf
Binary files differ
diff --git a/benchmark/app/src/main/res/layout/bottom_sheet_dialog.xml b/benchmark/app/src/main/res/layout/bottom_sheet_dialog.xml
new file mode 100644
index 0000000..07539b9
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/bottom_sheet_dialog.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="@color/grass"
+ android:id="@+id/slideUpContainer"
+ app:behavior_hideable="false"
+ app:behavior_peekHeight="65dp"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="60dp"
+ android:background="@drawable/list_handle_layout"
+ android:text="@string/divider_text"
+ android:gravity="center|center"
+ />
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/listContainer">
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" android:id="@+id/greenwashed">
+ </androidx.recyclerview.widget.RecyclerView>
+ </ScrollView>
+</LinearLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/card_layout.xml b/benchmark/app/src/main/res/layout/card_layout.xml
new file mode 100644
index 0000000..28154a7
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/card_layout.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" android:padding="5dp" android:layout_margin="5dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="4dp"
+ android:background="@drawable/card_bg">
+
+ <ImageView
+ android:contentDescription="@string/bench_image" android:src="@drawable/ic_bench_icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp" android:id="@+id/benchPfp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
+ <TextView
+ android:text="@string/bench_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/benchName"
+ app:layout_constraintStart_toEndOf="@id/benchPfp" app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/benchPos"/>
+ <TextView
+ android:text="@string/bench_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/benchPos"
+ app:layout_constraintTop_toBottomOf="@id/benchName"
+ app:layout_constraintBottom_toTopOf="@id/benchRating"
+ app:layout_constraintStart_toEndOf="@id/benchPfp" app:layout_constraintEnd_toEndOf="parent"/>
+ <RatingBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/benchRating"
+ android:isIndicator="true"
+ app:layout_constraintStart_toEndOf="@id/benchPfp" app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/benchPos" app:layout_constraintBottom_toBottomOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.cardview.widget.CardView>
diff --git a/benchmark/app/src/main/res/layout/dialog_bench_details.xml b/benchmark/app/src/main/res/layout/dialog_bench_details.xml
new file mode 100644
index 0000000..7ded3bd
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/dialog_bench_details.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/dialogTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Bench Name"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/dialogMessage"/>
+
+ <TextView
+ android:id="@+id/dialogMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Loading information..."
+ android:layout_marginBottom="8dp"
+ app:layout_constraintTop_toBottomOf="@id/dialogTitle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/benchImageView" />
+
+ <ImageView
+ android:id="@+id/benchImageView"
+ android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:scaleType="centerCrop"
+ android:layout_marginBottom="16dp"
+ android:contentDescription="Bench Image"
+ app:layout_constraintTop_toBottomOf="@id/dialogMessage"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/btnFirstRow"/>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:id="@+id/btnFirstRow"
+ app:layout_constraintTop_toBottomOf="@id/benchImageView"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/btnSecondRow">
+
+ <Button
+ android:id="@+id/btnAddReview"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="Nouvel avis"
+ android:layout_marginEnd="4dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/btnViewReviews" />
+
+ <Button
+ android:id="@+id/btnViewReviews"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Voir les avis"
+ android:layout_marginStart="4dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/btnAddReview"
+ app:layout_constraintEnd_toEndOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="8dp"
+ android:id="@+id/btnSecondRow"
+ app:layout_constraintTop_toBottomOf="@id/btnFirstRow"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <Button
+ android:id="@+id/btnShowPath"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Itinéraire"
+ android:layout_marginEnd="4dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/btnClose" />
+
+ <Button
+ android:id="@+id/btnClose"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Retour"
+ android:layout_marginStart="4dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/btnShowPath"
+ app:layout_constraintEnd_toEndOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/dialog_review.xml b/benchmark/app/src/main/res/layout/dialog_review.xml
new file mode 100644
index 0000000..c43f746
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/dialog_review.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Note:"
+ android:textSize="16sp"
+ android:layout_marginBottom="8dp" />
+
+ <RatingBar
+ android:id="@+id/ratingBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:numStars="5"
+ android:stepSize="1.0"
+ android:layout_marginBottom="16dp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Commentaire (optionnel):"
+ android:textSize="16sp"
+ android:layout_marginBottom="8dp" />
+
+ <EditText
+ android:id="@+id/commentEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="Entrez votre commentaire ici"
+ android:inputType="textMultiLine"
+ android:minLines="3"
+ android:gravity="top|start" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/dialog_reviews_list.xml b/benchmark/app/src/main/res/layout/dialog_reviews_list.xml
new file mode 100644
index 0000000..20f211b
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/dialog_reviews_list.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/reviewsTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Avis"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/averageRatingText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Note moyenne: 0.0/5"
+ android:textSize="16sp"
+ android:layout_marginBottom="8dp" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/reviewsRecyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp" />
+
+ <TextView
+ android:id="@+id/noReviewsText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Aucun avis pour le moment"
+ android:textSize="16sp"
+ android:textAlignment="center"
+ android:visibility="gone" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/item_review.xml b/benchmark/app/src/main/res/layout/item_review.xml
new file mode 100644
index 0000000..71d40e1
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/item_review.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="#F5F5F5">
+
+ <RatingBar
+ android:id="@+id/itemRatingBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:isIndicator="true"
+ style="?android:attr/ratingBarStyleSmall"
+ android:numStars="5"
+ android:stepSize="1.0"
+ android:layout_marginBottom="4dp" />
+
+ <TextView
+ android:id="@+id/itemCommentText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/main_layout.xml b/benchmark/app/src/main/res/layout/main_layout.xml
new file mode 100644
index 0000000..a399a07
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/main_layout.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/root_layout"
+ android:gravity="bottom">
+ <org.osmdroid.views.MapView android:id="@+id/map"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent">
+ </org.osmdroid.views.MapView>
+ <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:orientation="vertical" android:layout_alignParentEnd="true" android:layout_marginEnd="10dp"
+ android:layout_centerVertical="true">
+ <ImageButton
+ android:contentDescription="@string/button_content" android:src="@drawable/plus_5345954"
+ android:layout_width="50dp"
+ android:layout_height="50dp" android:id="@+id/plusBtn"
+ android:layout_gravity="end|bottom"/>
+ <ImageButton
+ android:contentDescription="@string/button_content" android:src="@drawable/target"
+ android:layout_width="50dp"
+ android:layout_height="50dp" android:id="@+id/posBtn"
+ android:layout_gravity="end|bottom"/>
+ </LinearLayout>
+ <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:id="@+id/listWrapper">
+ <include layout="@layout/bottom_sheet_dialog"/>
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/layout/new_bench_layout.xml b/benchmark/app/src/main/res/layout/new_bench_layout.xml
new file mode 100644
index 0000000..05d0f25
--- /dev/null
+++ b/benchmark/app/src/main/res/layout/new_bench_layout.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+ android:layout_height="350dp"
+ android:layout_marginHorizontal="25dp"
+ android:background="@drawable/card_bg">
+
+ <TextView
+ android:text="@string/new_bench"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/title"
+ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/editTextTextMultiLine"
+ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="200dp"
+ android:autofillHints="" android:inputType="textMultiLine"
+ android:gravity="start|top"
+ android:ems="10"
+ android:id="@+id/editTextTextMultiLine"
+ app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toTopOf="@id/ratingBar"
+ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
+ tools:ignore="LabelFor"/>
+ <RatingBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/ratingBar"
+ app:layout_constraintTop_toBottomOf="@id/editTextTextMultiLine"
+ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
+ <Button
+ android:text="@string/save"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/submit"
+ android:layout_marginEnd="20dp"
+ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/ratingBar"
+ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="@id/cancel"/>
+ <Button
+ android:text="@string/cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:id="@+id/cancel"
+ android:layout_marginStart="20dp"
+ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/ratingBar"
+ app:layout_constraintStart_toStartOf="@id/submit" app:layout_constraintEnd_toEndOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..50ec886
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+ <monochrome android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..50ec886
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+ <monochrome android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/benchmark/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/benchmark/app/src/main/res/values/colors.xml b/benchmark/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6482580
--- /dev/null
+++ b/benchmark/app/src/main/res/values/colors.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+ <color name="salmon">#FF8C69</color>
+ <color name="grass">#81c9b5</color>
+ <color name="lagoon">#90d8c4</color>
+</resources> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/values/strings.xml b/benchmark/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f5059a1
--- /dev/null
+++ b/benchmark/app/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<resources>
+ <string name="app_name">BenchMark</string>
+ <string name="bench1">Banc n°1</string>
+ <string name="bench2">Banc n°2</string>
+ <string name="bench3">Banc n°3</string>
+ <string name="divider_text">Haut !</string>
+ <string name="bench_image">Bench image</string>
+ <string name="new_bench">Nouveau banc</string>
+ <string name="save">Enregistrer</string>
+ <string name="cancel">Annuler</string>
+ <string name="devs">Martial Simon - Ethan Lefevre</string>
+ <string name="quote">\'Tite bière ?</string>
+ <string name="bench_name">Nom du Banc</string>
+ <string name="bench_position">Position du banc</string>
+ <string name="addPosText">+</string>
+ <string name="getPosText">0</string>
+ <string name="button_content">Un bouton pour les gouverner tous</string>
+</resources> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/values/themes.xml b/benchmark/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..076cab1
--- /dev/null
+++ b/benchmark/app/src/main/res/values/themes.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Theme.Benchmark"
+ parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:fontFamily">@font/nokia_font</item>
+ </style>
+ <style name="Theme.AppSplash" parent="Theme.SplashScreen">
+ <item name="windowSplashScreenBackground">@color/grass</item>
+ <item name="windowSplashScreenAnimatedIcon">@drawable/ic_bench_icon</item>
+ <item name="postSplashScreenTheme">@style/Theme.Benchmark</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/xml/backup_rules.xml b/benchmark/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..120dbfe
--- /dev/null
+++ b/benchmark/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Sample backup rules file; uncomment and customize as necessary.
+ See https://developer.android.com/guide/topics/data/autobackup
+ for details.
+ Note: This file is ignored for devices older than API 31
+ See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+ <!--
+ <include domain="sharedpref" path="."/>
+ <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/xml/data_extraction_rules.xml b/benchmark/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..a73ffe1
--- /dev/null
+++ b/benchmark/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Sample data extraction rules file; uncomment and customize as necessary.
+ See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+ for details.
+-->
+<data-extraction-rules>
+ <cloud-backup>
+ <!-- TODO: Use <include> and <exclude> to control what is backed up.
+ <include .../>
+ <exclude .../>
+ -->
+ </cloud-backup>
+ <!--
+ <device-transfer>
+ <include .../>
+ <exclude .../>
+ </device-transfer>
+ -->
+</data-extraction-rules> \ No newline at end of file
diff --git a/benchmark/app/src/main/res/xml/network_security_config.xml b/benchmark/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..fe6acf6
--- /dev/null
+++ b/benchmark/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">89.168.39.144</domain>
+ <domain includeSubdomains="true">la-banquise.fr</domain>
+ </domain-config>
+</network-security-config> \ No newline at end of file
diff --git a/benchmark/app/src/test/java/io/trentetroim/benchmark/ExampleUnitTest.kt b/benchmark/app/src/test/java/io/trentetroim/benchmark/ExampleUnitTest.kt
new file mode 100644
index 0000000..b8377dd
--- /dev/null
+++ b/benchmark/app/src/test/java/io/trentetroim/benchmark/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package io.trentetroim.benchmark
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+} \ No newline at end of file