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
41 changes: 24 additions & 17 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ on:

jobs:
build-and-test:
runs-on: macos-latest
env:
ANDROID_SDK_ROOT: /Users/runner/Library/Android/sdk
runs-on: ubuntu-latest

steps:
- name: Checkout repository
Expand All @@ -35,35 +33,44 @@ jobs:
distribution: temurin
java-version: 17

- name: Enable KVM
run: |
sudo apt-get update
sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
sudo adduser $USER libvirt
sudo adduser $USER kvm
sudo chmod 666 /dev/kvm
ls -l /dev/kvm

- name: Install Android SDK
uses: android-actions/setup-android@v2
with:
api-level: 33
build-tools: "33.0.2"
components: |
platform-tools
emulator
tools
platforms;android-33
system-images;android-33;google_apis;x86_64
uses: android-actions/setup-android@v3

- name: Install Android SDK packages
run: |
sdkmanager --install \
"platform-tools" \
"platforms;android-33" \
"build-tools;33.0.2" \
"emulator" \
"system-images;android-33;google_apis;x86_64"

- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses

- name: Create and start emulator
- name: Create and start emulator, run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: google_apis
arch: x86_64
force-avd-creation: true
emulator-options: -no-window -no-boot-anim -gpu swiftshader_indirect
emulator-options: -no-window -no-boot-anim -noaudio -gpu swiftshader_indirect -no-snapshot -no-snapshot-save -wipe-data -accel on
disable-animations: true
emulator-boot-timeout: 180
script: ./gradlew connectedDebugAndroidTest --stacktrace

- name: Build debug APK, run unit & instrumentation tests
- name: Build debug APK, run unit tests
run: |
./gradlew clean assembleDebug \
testDebugUnitTest \
connectedDebugAndroidTest \
--stacktrace
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
local.properties
.idea/
.gradle/
.kotlin/
internalDocs/
gradle/local-properites.kts
out.txt
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Back-end часть проекта доступна по ссылке:

- [SmartCalendarServer](https://github.com/hse-project-Java-2025/server)

## Video
- AI-assist demonstration
https://github.com/user-attachments/assets/963f8ed1-c95e-4326-ae8b-bdfbd8c01e74
- Main functionality demonstration
https://drive.google.com/file/d/1Zq_mHprqxPhZRkU3m5l3qvVPfQEbaJFy/view?usp=sharing

# Key Features

## 👤 User Authentication & Profile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class AchievementsScreenTest {
fun achievementsShowsStreak() {
val statisticsViewModel = StatisticsViewModel()
val listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel))
listViewModel.loadDailyTasks()
//нужно потестить каждый элемент:Planning everything - без заданий 0,
// с заданием 5ч 5/10, c 24ч 24 часа
composeTestRule.setContent {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package org.hse.smartcalendar.ui.screens

import androidx.compose.ui.test.*
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hse.smartcalendar.activity.MainActivity
import org.hse.smartcalendar.data.WorkManagerHolder
import org.hse.smartcalendar.ui.navigation.App
import org.hse.smartcalendar.ui.theme.SmartCalendarTheme
import org.hse.smartcalendar.view.model.ListViewModel
import org.hse.smartcalendar.view.model.StatisticsManager
import org.hse.smartcalendar.view.model.StatisticsViewModel
import org.hse.smartcalendar.view.model.TaskEditViewModel
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -12,11 +22,25 @@ import org.junit.runner.RunWith
class GreetingFlowTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun testLogin() {
composeTestRule.waitForIdle()
WorkManagerHolder.init(composeTestRule.activity)
val statisticsViewModel = StatisticsViewModel()
val listViewModel = ListViewModel(StatisticsManager(statisticsViewModel))
val taskEditViewModel = TaskEditViewModel(listViewModel)

composeTestRule.setContent {
SmartCalendarTheme {
App(
statisticsVM = statisticsViewModel,
listVM = listViewModel,
taskEditVM = taskEditViewModel
)
}
}

composeTestRule.onNodeWithTag("authorizationButtonTest")
.assertIsDisplayed()
.performClick()
Expand All @@ -27,13 +51,9 @@ class GreetingFlowTest {
composeTestRule.onNodeWithTag("passwordField")
.assertIsDisplayed()
.performTextInput("superpassword")
composeTestRule.onNodeWithTag("passwordField")
.performImeAction()
composeTestRule.onNodeWithTag("loginSubmitButton")
.assertIsDisplayed()
.performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("loginField")
.assertDoesNotExist()
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ class DailySchedule (val date : LocalDate = Clock.System.now()
{
private var dailyTasksList: LinkedList<DailyTask> = LinkedList()

fun tryAddTask(newTask : DailyTask) : Boolean {
val iterator = dailyTasksList.iterator()
iterator.forEach { task ->
if (task.isNestedTasks(newTask)) {
return false
}
}
return true
}
fun addDailyTask(newTask : DailyTask) : Boolean {
val iterator = dailyTasksList.iterator()
iterator.forEach { task ->
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/org/hse/smartcalendar/data/SuggestedTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.hse.smartcalendar.data

data class SuggestedTask (
val task: DailyTask,
var status: TaskStatus = TaskStatus.PENDING){
enum class TaskStatus { PENDING, ACCEPTED, DECLINED }
}
4 changes: 4 additions & 0 deletions app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import java.time.OffsetDateTime

object ApiClient {
private const val SERVER_BASE_URL = "http://10.0.2.2:8080/"
//в случае работы с реальным телефоном адрес сервера это адрес сети wi-fi, где запущен docker
var authToken: String? = null
private val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor { authToken })
Expand Down Expand Up @@ -53,6 +54,9 @@ object ApiClient {
val inviteApiService: InviteApiInterface by lazy {
retrofit.create(InviteApiInterface::class.java)
}
val chatGptApiService: ChatGptApiInterface by lazy {
retrofit.create(ChatGptApiInterface::class.java)
}
}
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,18 @@ interface InviteApiInterface {
@Path("eventId") eventId: UUID,
@Body request: InviteRequest
): Response<ResponseBody>
}

interface ChatGptApiInterface {
@POST("api/chatgpt/{userId}/generate/suggestions")
suspend fun generateSuggestions(
@Path("userId") userId: Long,
@Body request: ChatGptRequest
): Response<ChatGptResponse>
@Multipart
@POST("api/chatgpt/{userId}/generate/suggestions/audio")
suspend fun generateSuggestionsFromAudio(
@Path("userId") userId: Long,
@Part file: MultipartBody.Part
): Response<ChatGptResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,4 @@ data class ChatTaskResponse(
applyToState(newEnd, endTime)
}
}
data class ChatGptResponse(val events: List<ChatTaskResponse>)
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@ sealed class NetworkResponse<out T> {
return Error("token is null")
}
}
fun <R> mapSuccess(transform: (T) -> R): NetworkResponse<R> {
return when (this) {
is NetworkResponse.Success -> NetworkResponse.Success(transform(this.data))
is NetworkResponse.Error -> NetworkResponse.Error(this.message)
is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(this.exceptionMessage)
is NetworkResponse.Loading -> NetworkResponse.Loading
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
import org.hse.smartcalendar.data.DailyTask
import org.hse.smartcalendar.data.DailyTaskType
import org.hse.smartcalendar.utility.TimeUtils
data class ChatGptRequest(val query: String)
data class InviteRequest(val loginOrEmail: String)
data class LoginRequest(
val username: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.hse.smartcalendar.repository

import org.hse.smartcalendar.data.DailySchedule
import org.hse.smartcalendar.data.DailyTask
import org.hse.smartcalendar.data.User
import org.hse.smartcalendar.network.ChatGptApiInterface
import org.hse.smartcalendar.network.ChatGptRequest
import org.hse.smartcalendar.network.NetworkResponse
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import org.hse.smartcalendar.network.AudioApiInterface
import org.hse.smartcalendar.network.ChatTaskResponse
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.RequestBody.Companion.asRequestBody
import org.hse.smartcalendar.network.ChatGptResponse
import org.hse.smartcalendar.store.StatisticsStore


class ChatGptRepository(
private val api: ChatGptApiInterface
) : BaseRepository() {

val mainSchedule = User.getSchedule()
private val id = User.id

suspend fun generateSuggestions(query: String): NetworkResponse<List<DailyTask>> {
return withIdRequest { userId ->
api.generateSuggestions(userId, ChatGptRequest(query))
}.mapSuccess { response ->
response.events.map { it.toDailyTask() }
}
}
suspend fun generateSuggestionsAudio(audioFile: File): NetworkResponse<List<DailyTask>> {
return withIdRequest { userId ->
val requestFile = audioFile.asRequestBody("audio/*".toMediaType())
val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile)
api.generateSuggestionsFromAudio(userId, part)
}.mapSuccess { response ->
response.events.map { it.toDailyTask() }
}
}


fun tryAdd(task: DailyTask): Boolean {
return mainSchedule.getOrCreateDailySchedule(task.getTaskDate()).tryAddTask(task)
}

fun declineTask(task: DailyTask): Boolean {
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.hse.smartcalendar.ui.elements
import android.Manifest
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
Expand All @@ -29,13 +31,16 @@ import java.io.File
fun AudioRecorderButton(
modifier: Modifier = Modifier,
audioFile: MutableState<File?>,
onStop: () -> Unit
onStop: () -> Unit,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
iconColor: Color = Color.White
) {
val recordingState = rememberSaveable { mutableStateOf(RecordState.IDLE) }
val context = LocalContext.current
val audioPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO)

Button(
colors = buttonColors,
modifier = modifier,
onClick = {
when {
Expand Down Expand Up @@ -65,15 +70,15 @@ fun AudioRecorderButton(
Icon(
painter = painterResource(R.drawable.mic_asset),
contentDescription = "Recording",
tint = Color.White,
tint = iconColor,
modifier = Modifier.size(24.dp)
)

RecordState.RECORDING ->
Icon(
painter = painterResource(R.drawable.stop_asset),
contentDescription = "Recording",
tint = Color.White,
tint = iconColor,
modifier = Modifier.size(24.dp)
)

Expand Down
Loading