Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 RouteMood

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# RouteMood - Генератор маршрутов для Android

[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

## Содержание
- [Описание](#описание)
- [Возможности](#возможности)
- [Скриншоты](#скриншоты)
- [Технологии](#технологии)
- [Установка и запуск](#установка-и-запуск)
- [Лицензия](#лицензия)

## Описание
Android-приложение для создания и оценки пеших маршрутов. Позволяет генерировать маршруты с помощью YandexGPT или создавать их вручную на карте.

## Возможности
✔ Генерация маршрутов через YandexGPT
✔ Ручное создание маршрутов на карте
✔ Сохранение маршрутов локально (Room Database)
✔ Обмен маршрутами с другими пользователями
✔ Рейтинговая система (1-5 звёзд)
✔ Пользовательские аватары (Datastore + сервер)
✔ Воспроизведение видео при генерации маршрута

## Скриншоты
| Экран входа | Настройки маршрута | Профиль |
|-------------|-------------------|---------|
| <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlJA98inIg-p0bALx3hnMtseZycTqESwTe19UxnT3E5CcTRmhFByGkhUTPomM6_88u4DR9jRnILGqMBoz_zffRIxfAAnnfXDWUScESXFuFWTwvqryt0a2w=s512" width="200"/> | <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlJ2faS1d_ZddRJVyVnSi3VGY6D8VUs0ihYITypEzlqNadjjpBb-m5InXKUCrSA3QgOVORx7AxlBUReIz58slAiFhJbQdnogRMKje4AbFs3VpNzoKo4HYA=s512" width="200"/> | <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlJZTP4eEkB0q54aYoqXXhnECQNg13jOFbQUXurAFGny4pu4eLhAyb_M4Odwh19Eq-cbEruzpcrWJ40w8WGJrjzUFE1i3vlMYHGCVOvJBXlzoEBzm65J7Q=s2048" width="200"/> |

| Создание маршрута | Просмотр маршрута | Рейтинг |
|-------------------|------------------|---------|
| <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlJ6UJmdXGHydamiZOtDx3PBRpNFhWjUCzfVlzzqKVBTwErKGWexXw6xcsKduLjKKvmDxPPsdd6U-gXjrZpHXuFinB3ShsWyHu05tmqrdlH1ytYvLYBqaw=s2048" width="200"/> | <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlLYzn_xD2CaDe-BVTJl-r0i3P6VOoxD6nmnRcnv1atkMh0UJV-0XH_nbR8KglvcULRoRs1C8LY5QqMDeIJ5MRypJHKQwDQHgtLBwYsijMvR1YEoudVO-Q=s2048" width="200"/> | <img src="https://lh3.googleusercontent.com/keep-bbsk/AFgXFlLvJvKL5XjkivkClluPaswBUxt-mSmh18i88gx826dR_Q7vIBa8KIMo7eXptc0rZ6Ad5Dm14QNTR-OOcNsm3nA15fwZfDpJplb4Ehcm92igzWRGWcK5RQ=s2048" width="200"/> |

## Технологии
- Язык: Kotlin, Jetpack Compose
- Карты: Google Maps SDK
- Локальное хранилище: Room Database, DataStore
- Сетевое взаимодействие: Retrofit
- Генерация маршрутов: YandexGPT API
- Архитектура: MVVM

## Установка и запуск
1. Установите [Android Studio](https://developer.android.com/studio)
2. Клонируйте репозиторий:
```bash
git clone https://github.com/RouteMood/RouteMoodClient.git
3. Откройте проект в Android Studio

4. Добавьте API ключи:

Создайте файл local.default.properties в корне проекта
```properties
# Ключ для Google Maps SDK
MAPS_API_KEY=your_google_maps_key

# Ключ для Google Directions API
MAPS_SERVER_API_KEY=your_directions_api_key
```
# Лицензия
MIT License. Подробнее см. в файле LICENSE.

15 changes: 14 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ android {

defaultConfig {
applicationId = "ru.hse.routemoodclient"
minSdk = 24
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
Expand Down Expand Up @@ -61,6 +61,19 @@ android {
}

dependencies {
// Test
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.room:room-testing:2.6.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
testImplementation("app.cash.turbine:turbine:1.0.0")
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
// DataStore
implementation("androidx.datastore:datastore-preferences:1.1.7")
// Glide
implementation ("com.github.bumptech.glide:compose:1.0.0-beta01")
// Permissions
implementation ("com.google.accompanist:accompanist-permissions:0.37.0")
// Gson
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package ru.hse.routemoodclient.imageManagerTest

import android.content.Context
import android.net.Uri
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import ru.hse.routemoodclient.ui.ImageManager
import java.io.File
import java.util.UUID

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ImageManagerTest {
private lateinit var imageManager: ImageManager
private lateinit var context: Context
private lateinit var dataStore: DataStore<Preferences>
private val testDataStoreName = "test_image_prefs_${System.currentTimeMillis()}"

@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()

// Создаем уникальное имя для DataStore в каждом тесте
dataStore = PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile(testDataStoreName) }
)

// Очищаем данные перед каждым тестом
runBlocking {
dataStore.edit { it.clear() }
File(context.filesDir, ImageManager.IMAGE_DIR).deleteRecursively()
}

imageManager = ImageManager(dataStore, context)
}

@After
fun cleanup() {
runBlocking {
// Удаляем тестовый DataStore
File(context.filesDir, "datastore/$testDataStoreName.preferences_pb").delete()
File(context.filesDir, ImageManager.IMAGE_DIR).deleteRecursively()
}
}


@Test
fun addAndGetUuidUriPair() = runTest {
// Создаем тестовый файл изображения
val testFile = File(context.filesDir, "test_image.jpg")
testFile.createNewFile()
testFile.writeText("test image content")
val testUri = Uri.fromFile(testFile)

val uuid = UUID.randomUUID()
val savedFile = imageManager.addUuidUriPair(uuid, testUri)

// Проверяем блокирующую версию
val retrievedFile = imageManager.getFileForUuidBlocking(uuid)
assertEquals(savedFile.absolutePath, retrievedFile?.absolutePath)
assertTrue(retrievedFile?.exists() ?: false)

// Проверяем Flow версию
val flowFile = imageManager.getFileForUuid(uuid).first()
assertEquals(savedFile.absolutePath, flowFile?.absolutePath)
}

@Test
fun addAndGetUuidByteArrayPair() = runTest {
val uuid = UUID.randomUUID()
val testData = "test byte array content".toByteArray()

val savedFile = imageManager.addUuidByteArrayPair(uuid, testData)

// Проверяем содержимое файла
val fileContent = savedFile.readBytes()
assertTrue(fileContent.contentEquals(testData))

// Проверяем Uri
val uri = imageManager.getUriForUuidBlocking(uuid)
assertNotNull(uri)
}

@Test
fun removeUuidUriPair() = runTest {
val uuid = UUID.randomUUID()
val testData = "test content".toByteArray()
val file = imageManager.addUuidByteArrayPair(uuid, testData)

// Убеждаемся, что файл существует
assertTrue(file.exists())

// Удаляем
imageManager.removeUuidUriPair(uuid)

// Проверяем, что файл удален
assertFalse(file.exists())

// Проверяем, что запись удалена из DataStore
val path = dataStore.data.map { it[imageManager.uuidKey(uuid)] }.first()
assertNull(path)
}

@Test
fun getAllPairs() = runTest {
val uuid1 = UUID.randomUUID()
val uuid2 = UUID.randomUUID()

imageManager.addUuidByteArrayPair(uuid1, "image1".toByteArray())
imageManager.addUuidByteArrayPair(uuid2, "image2".toByteArray())

val allPairs = imageManager.getAllPairs().first()

assertEquals(2, allPairs.size)
assertTrue(allPairs.containsKey(uuid1))
assertTrue(allPairs.containsKey(uuid2))
}

@Test
fun getUriForUuid() = runTest {
val uuid = UUID.randomUUID()
val testData = "test uri content".toByteArray()
imageManager.addUuidByteArrayPair(uuid, testData)

// Проверяем блокирующую версию
val blockingUri = imageManager.getUriForUuidBlocking(uuid)
assertNotNull(blockingUri)

// Проверяем Flow версию
val flowUri = imageManager.getUriForUuid(uuid).first()
assertEquals(blockingUri, flowUri)
}

@Test
fun fileNotExistsAfterRemoval() = runTest {
val uuid = UUID.randomUUID()
val file = imageManager.addUuidByteArrayPair(uuid, "test".toByteArray())

// Удаляем
imageManager.removeUuidUriPair(uuid)

// Проверяем, что файл действительно удален
assertFalse(file.exists())
}

@Test
fun addInvalidUriThrowsException() = runTest {
val uuid = UUID.randomUUID()
val invalidUri = Uri.parse("content://invalid.uri")

assertThrows(java.io.FileNotFoundException::class.java) {
runBlocking {
imageManager.addUuidUriPair(uuid, invalidUri)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ru.hse.routemoodclient.routesstoragetests

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.gms.maps.model.LatLng
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import ru.hse.routemoodclient.data.Converters

@RunWith(AndroidJUnit4::class)
class ConvertersTest {
private val converters = Converters()

@Test
fun testLatLngListConversion() {
val originalList = listOf(
LatLng(59.9386, 30.3141),
LatLng(55.7558, 37.6173)
)

val json = converters.fromLatLngList(originalList)
assertNotNull(json)

val convertedList = converters.toLatLngList(json)
assertEquals(originalList, convertedList)
}

@Test
fun testNullConversion() {
assertEquals("null", converters.fromLatLngList(null))
assertNull(converters.toLatLngList(null))
}

@Test
fun testEmptyListConversion() {
val emptyList = emptyList<LatLng>()
val json = converters.fromLatLngList(emptyList)
val convertedList = converters.toLatLngList(json)
assertEquals(emptyList, convertedList)
}
}
Loading