haunt / commit
Layer 2: RTT ranging, debug map, add/rename/delete points
app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 2 +- .../main/java/net/fuzzyworld/haunt/MainActivity.kt | 78 ++++++++--- .../java/net/fuzzyworld/haunt/debug/DebugPoint.kt | 8 ++ .../net/fuzzyworld/haunt/debug/DebugPointStore.kt | 26 ++++ .../net/fuzzyworld/haunt/debug/DebugViewModel.kt | 68 +++++++++ .../java/net/fuzzyworld/haunt/debug/MapView.kt | 153 +++++++++++++++++++++ .../net/fuzzyworld/haunt/debug/PointsAdapter.kt | 40 ++++++ .../fuzzyworld/haunt/ranging/RttRangingManager.kt | 46 +++++++ app/src/main/res/layout/activity_main.xml | 53 ++++--- app/src/main/res/layout/item_debug_point.xml | 34 +++++ app/src/main/res/values/themes.xml | 7 + gradle.properties | 11 ++ gradle/libs.versions.toml | 11 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 489 insertions(+), 54 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 188f1b7..6da8c40 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,10 @@ android { } dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.recyclerview) implementation(libs.kotlinx.coroutines.android) implementation(libs.retrofit) implementation(libs.retrofit.gson) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08a4ca7..ad9605e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.AppCompat.Light.DarkActionBar"> + android:theme="@style/Theme.Haunt"> <activity android:name=".MainActivity" diff --git a/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt b/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt index 7e8e5d6..3b1ca30 100644 --- a/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt +++ b/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt @@ -2,25 +2,36 @@ package net.fuzzyworld.haunt import android.Manifest import android.content.pm.PackageManager -import android.net.wifi.rtt.WifiRttManager import android.os.Bundle -import android.widget.Toast +import android.widget.EditText import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import net.fuzzyworld.haunt.databinding.ActivityMainBinding +import net.fuzzyworld.haunt.debug.DebugViewModel +import net.fuzzyworld.haunt.debug.PointsAdapter class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val vm: DebugViewModel by viewModels() + + private val adapter = PointsAdapter( + onRename = { id, name -> vm.renamePoint(id, name) }, + onDelete = { id -> vm.deletePoint(id) }, + ) private val requestPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> - if (granted) { - checkRttAvailability() - } else { - Toast.makeText(this, R.string.permission_required, Toast.LENGTH_LONG).show() + if (!granted) { + binding.readingsText.text = "Location permission required for RTT ranging" } } @@ -29,27 +40,52 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - binding.calibrateButton.setOnClickListener { - binding.modeText.text = "Mode: Calibrate (not yet implemented)" - } - binding.liveButton.setOnClickListener { - binding.modeText.text = "Mode: Live (not yet implemented)" - } + binding.pointsList.layoutManager = LinearLayoutManager(this) + binding.pointsList.adapter = adapter + + binding.addPointButton.setOnClickListener { showAddPointDialog() } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED + != PackageManager.PERMISSION_GRANTED ) { - checkRttAvailability() - } else { requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) } + + lifecycleScope.launch { + vm.readings.collectLatest { readings -> + binding.readingsText.text = if (readings.isEmpty()) { + "Scanning… (no RTT-capable APs found)" + } else { + readings.joinToString("\n") { "…${it.bssid.takeLast(8)} ${String.format("%.2f", it.distanceMeters)} m" } + } + binding.mapView.currentReadings = readings.associate { it.bssid to it.distanceMeters } + } + } + + lifecycleScope.launch { + vm.points.collectLatest { points -> + adapter.submitList(points) + binding.mapView.savedPoints = points + } + } } - private fun checkRttAvailability() { - val rttManager = getSystemService(WifiRttManager::class.java) - val available = rttManager?.isAvailable == true - binding.rttStatusText.text = getString( - if (available) R.string.rtt_available else R.string.rtt_unavailable - ) + private fun showAddPointDialog() { + val input = EditText(this).apply { + hint = "Point name" + setPadding(48, 24, 48, 8) + val count = vm.points.value.size + 1 + setText("Point $count") + selectAll() + } + AlertDialog.Builder(this) + .setTitle("Name this location") + .setView(input) + .setPositiveButton("Add") { _, _ -> + val name = input.text.toString().trim().ifEmpty { "Point" } + vm.addPoint(name) + } + .setNegativeButton("Cancel", null) + .show() } } diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPoint.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPoint.kt new file mode 100644 index 0000000..3747a2c --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPoint.kt @@ -0,0 +1,8 @@ +package net.fuzzyworld.haunt.debug + +data class DebugPoint( + val id: String, + val name: String, + val color: Int, + val readings: Map<String, Double>, // BSSID -> meters +) diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPointStore.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPointStore.kt new file mode 100644 index 0000000..b1f49c1 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugPointStore.kt @@ -0,0 +1,26 @@ +package net.fuzzyworld.haunt.debug + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.io.File + +class DebugPointStore(context: Context) { + + private val file = File(context.filesDir, "debug_points.json") + private val gson = Gson() + + fun load(): List<DebugPoint> { + if (!file.exists()) return emptyList() + return try { + val type = object : TypeToken<List<DebugPoint>>() {}.type + gson.fromJson<List<DebugPoint>>(file.readText(), type) ?: emptyList() + } catch (_: Exception) { + emptyList() + } + } + + fun save(points: List<DebugPoint>) { + file.writeText(gson.toJson(points)) + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/DebugViewModel.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugViewModel.kt new file mode 100644 index 0000000..fd79791 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/DebugViewModel.kt @@ -0,0 +1,68 @@ +package net.fuzzyworld.haunt.debug + +import android.app.Application +import android.graphics.Color +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import net.fuzzyworld.haunt.ranging.RttRangingManager +import java.util.UUID + +class DebugViewModel(application: Application) : AndroidViewModel(application) { + + private val rttManager = RttRangingManager(application) + private val store = DebugPointStore(application) + + private val _readings = MutableStateFlow<List<RttRangingManager.ApReading>>(emptyList()) + val readings: StateFlow<List<RttRangingManager.ApReading>> = _readings + + private val _points = MutableStateFlow<List<DebugPoint>>(emptyList()) + val points: StateFlow<List<DebugPoint>> = _points + + val isRttAvailable: Boolean get() = rttManager.isAvailable + + init { + _points.value = store.load() + startRanging() + } + + private fun startRanging() { + viewModelScope.launch { + while (true) { + _readings.value = rttManager.rangeAll() + delay(2000) + } + } + } + + fun addPoint(name: String) { + val current = _readings.value + if (current.isEmpty()) return + val point = DebugPoint( + id = UUID.randomUUID().toString(), + name = name, + color = randomColor(), + readings = current.associate { it.bssid to it.distanceMeters }, + ) + _points.value = _points.value + point + store.save(_points.value) + } + + fun renamePoint(id: String, newName: String) { + _points.value = _points.value.map { if (it.id == id) it.copy(name = newName) else it } + store.save(_points.value) + } + + fun deletePoint(id: String) { + _points.value = _points.value.filter { it.id != id } + store.save(_points.value) + } + + private fun randomColor(): Int { + val hue = (Math.random() * 360).toFloat() + return Color.HSVToColor(floatArrayOf(hue, 0.70f, 0.80f)) + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt new file mode 100644 index 0000000..28a019c --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt @@ -0,0 +1,153 @@ +package net.fuzzyworld.haunt.debug + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.View +import kotlin.math.roundToInt + +class MapView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : View(context, attrs) { + + var savedPoints: List<DebugPoint> = emptyList() + set(value) { field = value; invalidate() } + + // Live position — rendered as a moving point on the same map, same visual language + var currentReadings: Map<String, Double> = emptyMap() + set(value) { field = value; invalidate() } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE } + private val gridPaint = Paint().apply { + color = Color.parseColor("#E0E0E0") + strokeWidth = 1.5f + } + private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.BLACK + textSize = 30f + typeface = Typeface.DEFAULT_BOLD + } + private val dimLabelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#888888") + textSize = 24f + } + private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 3f + pathEffect = DashPathEffect(floatArrayOf(8f, 6f), 0f) + } + + private val allBssids: List<String> + get() = (savedPoints.flatMap { it.readings.keys } + currentReadings.keys) + .distinct().sorted() + + private fun coords(readings: Map<String, Double>): Pair<Double, Double>? { + val bssids = allBssids + val x = bssids.getOrNull(0)?.let { readings[it] } ?: return null + val y = bssids.getOrNull(1)?.let { readings[it] } ?: 0.0 + return Pair(x, y) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.parseColor("#F2F2F2")) + + val currentCoords = if (currentReadings.isNotEmpty()) coords(currentReadings) else null + val savedCoords = savedPoints.mapNotNull { p -> coords(p.readings)?.let { p to it } } + + val allCoords = savedCoords.map { it.second } + listOfNotNull(currentCoords) + + if (allCoords.isEmpty()) { + labelPaint.color = Color.parseColor("#AAAAAA") + labelPaint.textAlign = Paint.Align.CENTER + canvas.drawText("No points yet", width / 2f, height / 2f - 16f, labelPaint) + dimLabelPaint.textAlign = Paint.Align.CENTER + canvas.drawText("walk somewhere and tap Add", width / 2f, height / 2f + 24f, dimLabelPaint) + return + } + + val padPx = 72f + val minX = (allCoords.minOf { it.first } - 1.0).coerceAtLeast(0.0) + val maxX = allCoords.maxOf { it.first } + 1.0 + val minY = (allCoords.minOf { it.second } - 1.0).coerceAtLeast(0.0) + val maxY = allCoords.maxOf { it.second } + 1.0 + + val scaleX = (width - 2 * padPx) / (maxX - minX).toFloat() + val scaleY = (height - 2 * padPx) / (maxY - minY).toFloat() + + fun sx(x: Double) = padPx + ((x - minX) * scaleX).toFloat() + fun sy(y: Double) = padPx + ((y - minY) * scaleY).toFloat() + + // Grid + for (gx in minX.roundToInt()..maxX.roundToInt()) { + canvas.drawLine(sx(gx.toDouble()), 0f, sx(gx.toDouble()), height.toFloat(), gridPaint) + dimLabelPaint.textAlign = Paint.Align.CENTER + canvas.drawText("${gx}m", sx(gx.toDouble()), height - 6f, dimLabelPaint) + } + for (gy in minY.roundToInt()..maxY.roundToInt()) { + canvas.drawLine(0f, sy(gy.toDouble()), width.toFloat(), sy(gy.toDouble()), gridPaint) + dimLabelPaint.textAlign = Paint.Align.LEFT + canvas.drawText("${gy}m", 4f, sy(gy.toDouble()) - 6f, dimLabelPaint) + } + + // Saved points + labelPaint.color = Color.BLACK + savedCoords.forEach { (point, xy) -> + val cx = sx(xy.first) + val cy = sy(xy.second) + + fillPaint.color = point.color + canvas.drawCircle(cx, cy, 26f, fillPaint) + strokePaint.color = Color.WHITE + strokePaint.strokeWidth = 3f + canvas.drawCircle(cx, cy, 26f, strokePaint) + + labelPaint.textAlign = Paint.Align.LEFT + canvas.drawText(point.name, cx + 34f, cy + 11f, labelPaint) + } + + // Current position — same visual as a point but white fill + dashed ring to show it's live + if (currentCoords != null) { + val cx = sx(currentCoords.first) + val cy = sy(currentCoords.second) + + fillPaint.color = Color.WHITE + canvas.drawCircle(cx, cy, 22f, fillPaint) + + strokePaint.color = Color.BLACK + strokePaint.strokeWidth = 4f + canvas.drawCircle(cx, cy, 22f, strokePaint) + + // Crosshair + strokePaint.strokeWidth = 2f + canvas.drawLine(cx - 38f, cy, cx + 38f, cy, strokePaint) + canvas.drawLine(cx, cy - 38f, cx, cy + 38f, strokePaint) + + // Dashed ring to distinguish from saved points + ringPaint.color = Color.parseColor("#888888") + canvas.drawCircle(cx, cy, 38f, ringPaint) + + labelPaint.textAlign = Paint.Align.LEFT + labelPaint.color = Color.parseColor("#444444") + canvas.drawText("you", cx + 46f, cy + 11f, labelPaint) + labelPaint.color = Color.BLACK + } + + // Axis labels + val bssids = allBssids + if (bssids.isNotEmpty()) { + dimLabelPaint.textAlign = Paint.Align.RIGHT + canvas.drawText("→ ${bssids[0].takeLast(5)}", width - 4f, height - 6f, dimLabelPaint) + } + if (bssids.size > 1) { + dimLabelPaint.textAlign = Paint.Align.LEFT + canvas.drawText("↓ ${bssids[1].takeLast(5)}", 4f, padPx - 10f, dimLabelPaint) + } + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt new file mode 100644 index 0000000..daa3cfc --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt @@ -0,0 +1,40 @@ +package net.fuzzyworld.haunt.debug + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import net.fuzzyworld.haunt.databinding.ItemDebugPointBinding + +class PointsAdapter( + private val onRename: (id: String, newName: String) -> Unit, + private val onDelete: (id: String) -> Unit, +) : RecyclerView.Adapter<PointsAdapter.ViewHolder>() { + + private var points: List<DebugPoint> = emptyList() + + fun submitList(list: List<DebugPoint>) { + points = list + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemDebugPointBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(points[position]) + override fun getItemCount() = points.size + + inner class ViewHolder(private val b: ItemDebugPointBinding) : RecyclerView.ViewHolder(b.root) { + fun bind(point: DebugPoint) { + b.colorSwatch.setBackgroundColor(point.color) + b.nameEdit.setText(point.name) + b.nameEdit.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) onRename(point.id, b.nameEdit.text.toString().trim().ifEmpty { point.name }) + } + b.deleteButton.setOnClickListener { onDelete(point.id) } + } + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/ranging/RttRangingManager.kt b/app/src/main/java/net/fuzzyworld/haunt/ranging/RttRangingManager.kt new file mode 100644 index 0000000..992e409 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/ranging/RttRangingManager.kt @@ -0,0 +1,46 @@ +package net.fuzzyworld.haunt.ranging + +import android.content.Context +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResult +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class RttRangingManager(private val context: Context) { + + data class ApReading(val bssid: String, val distanceMeters: Double) + + private val wifiManager = context.getSystemService(WifiManager::class.java) + private val rttManager = context.getSystemService(WifiRttManager::class.java) + + val isAvailable: Boolean get() = rttManager?.isAvailable == true + + suspend fun rangeAll(): List<ApReading> { + val manager = rttManager ?: return emptyList() + val scanResults = wifiManager?.scanResults?.filter { it.is80211mcResponder } + if (scanResults.isNullOrEmpty()) return emptyList() + + return suspendCoroutine { cont -> + val request = RangingRequest.Builder().apply { + scanResults.forEach { addAccessPoint(it) } + }.build() + + manager.startRanging(request, context.mainExecutor, object : RangingResultCallback() { + override fun onRangingResults(results: List<RangingResult>) { + val readings = results + .filter { it.status == RangingResult.STATUS_SUCCESS } + .map { ApReading(it.macAddress.toString(), it.distanceMm / 1000.0) } + .sortedBy { it.bssid } + cont.resume(readings) + } + + override fun onRangingFailure(code: Int) { + cont.resume(emptyList()) + } + }) + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c4108ad..0d4ea85 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -2,44 +2,39 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + android:fitsSystemWindows="true"> <TextView - android:id="@+id/rttStatusText" + android:id="@+id/readingsText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp" - android:textSize="16sp" /> + android:paddingHorizontal="12dp" + android:paddingTop="8dp" + android:paddingBottom="4dp" + android:fontFamily="monospace" + android:textSize="12sp" + android:text="Scanning…" /> - <TextView - android:id="@+id/modeText" + <net.fuzzyworld.haunt.debug.MapView + android:id="@+id/mapView" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="16dp" - android:text="Mode: —" - android:textSize="14sp" /> + android:layout_height="0dp" + android:layout_weight="1" /> - <LinearLayout + <Button + android:id="@+id/addPointButton" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="16dp"> - - <Button - android:id="@+id/calibrateButton" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:text="@string/tab_calibrate" /> + android:layout_marginHorizontal="12dp" + android:layout_marginVertical="6dp" + android:text="Add Point Here" /> - <Button - android:id="@+id/liveButton" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:layout_marginStart="8dp" - android:text="@string/tab_live" /> - - </LinearLayout> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/pointsList" + android:layout_width="match_parent" + android:layout_height="180dp" + android:clipToPadding="false" + android:paddingBottom="8dp" /> </LinearLayout> diff --git a/app/src/main/res/layout/item_debug_point.xml b/app/src/main/res/layout/item_debug_point.xml new file mode 100644 index 0000000..435275a --- /dev/null +++ b/app/src/main/res/layout/item_debug_point.xml @@ -0,0 +1,34 @@ +<?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="horizontal" + android:gravity="center_vertical" + android:paddingHorizontal="12dp" + android:paddingVertical="6dp"> + + <View + android:id="@+id/colorSwatch" + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_marginEnd="10dp" /> + + <EditText + android:id="@+id/nameEdit" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:background="@null" + android:singleLine="true" + android:textSize="15sp" + android:imeOptions="actionDone" /> + + <ImageButton + android:id="@+id/deleteButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:src="@android:drawable/ic_menu_delete" + android:contentDescription="Delete point" /> + +</LinearLayout> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..b089df1 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="Theme.Haunt" parent="Theme.AppCompat.Light.DarkActionBar"> + <!-- dark icons on the light status bar --> + <item name="android:windowLightStatusBar">true</item> + </style> +</resources> diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..be3e216 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +android.builtInKotlin=false +android.defaults.buildfeatures.resvalues=true +android.dependency.useConstraints=true +android.enableAppCompileTimeRClass=false +android.newDsl=false +android.r8.optimizedResourceShrinking=false +android.r8.strictFullModeForKeepRules=false +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.uniquePackageNames=false +android.useAndroidX=true +android.usesSdkInManifest.disallowed=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e09e4d8..a9eb983 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,18 @@ [versions] -agp = "8.7.3" -kotlin = "2.0.0" +agp = "9.2.1" +kotlin = "2.2.10" coroutines = "1.8.1" retrofit = "2.11.0" okhttp = "4.12.0" +appcompat = "1.7.0" +lifecycle = "2.8.7" +recyclerview = "1.3.2" [libraries] +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME