diff --git a/StudyBuddy/.gitignore b/StudyBuddy/.gitignore
new file mode 100644
index 00000000..aa724b77
--- /dev/null
+++ b/StudyBuddy/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/StudyBuddy/.idea/.gitignore b/StudyBuddy/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/StudyBuddy/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/StudyBuddy/.idea/.name b/StudyBuddy/.idea/.name
new file mode 100644
index 00000000..43b3e179
--- /dev/null
+++ b/StudyBuddy/.idea/.name
@@ -0,0 +1 @@
+Study Buddy
\ No newline at end of file
diff --git a/StudyBuddy/.idea/appInsightsSettings.xml b/StudyBuddy/.idea/appInsightsSettings.xml
new file mode 100644
index 00000000..23b2e1fe
--- /dev/null
+++ b/StudyBuddy/.idea/appInsightsSettings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/compiler.xml b/StudyBuddy/.idea/compiler.xml
new file mode 100644
index 00000000..b589d56e
--- /dev/null
+++ b/StudyBuddy/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/deploymentTargetSelector.xml b/StudyBuddy/.idea/deploymentTargetSelector.xml
new file mode 100644
index 00000000..55a93089
--- /dev/null
+++ b/StudyBuddy/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/gradle.xml b/StudyBuddy/.idea/gradle.xml
new file mode 100644
index 00000000..0897082f
--- /dev/null
+++ b/StudyBuddy/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/inspectionProfiles/Project_Default.xml b/StudyBuddy/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..fb93d254
--- /dev/null
+++ b/StudyBuddy/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/kotlinc.xml b/StudyBuddy/.idea/kotlinc.xml
new file mode 100644
index 00000000..fdf8d994
--- /dev/null
+++ b/StudyBuddy/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/migrations.xml b/StudyBuddy/.idea/migrations.xml
new file mode 100644
index 00000000..f8051a6f
--- /dev/null
+++ b/StudyBuddy/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/misc.xml b/StudyBuddy/.idea/misc.xml
new file mode 100644
index 00000000..8978d23d
--- /dev/null
+++ b/StudyBuddy/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/.idea/vcs.xml b/StudyBuddy/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/StudyBuddy/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/.gitignore b/StudyBuddy/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/StudyBuddy/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/StudyBuddy/app/build.gradle.kts b/StudyBuddy/app/build.gradle.kts
new file mode 100644
index 00000000..2b9b33b7
--- /dev/null
+++ b/StudyBuddy/app/build.gradle.kts
@@ -0,0 +1,112 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ id("com.google.dagger.hilt.android")
+ id("com.google.devtools.ksp")
+ alias(libs.plugins.google.gms.google.services)
+}
+
+android {
+ namespace = "com.example.studybuddy"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.example.studybuddy"
+ minSdk = 28
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation ("com.google.firebase:firebase-firestore:25.0.0") // Replace with latest compatible version
+ implementation ("com.google.firebase:firebase-auth:23.0.0") // Replace with latest compatible version
+ implementation ("com.google.firebase:firebase-storage:21.0.0") // Replace with latest compatible version
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.runtime.livedata)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.activity)
+ 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)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.androidx.lifecycle.runtime.compose)
+
+ //compose destination
+ val destinationVersion = "1.10.0"
+ implementation(libs.core)
+ ksp(libs.compose.destinations.ksp)
+
+ // Room
+ val roomVersion = "2.6.1"
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+
+ //Dagger-Hilt
+ implementation(libs.dagger.hilt.android)
+ ksp(libs.hilt.android.compiler)
+ ksp(libs.androidx.hilt.compiler)
+ implementation(libs.androidx.hilt.navigation.compose)
+
+ //fonts
+ implementation(libs.androidx.ui.text.google.fonts)
+
+
+ coreLibraryDesugaring(libs.desugar.jdk.libs)
+
+
+ implementation("com.mesibo.api:webrtc:1.0.5")
+ implementation("org.java-websocket:Java-WebSocket:1.5.7")
+ implementation("com.google.code.gson:gson:2.10.1")
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/google-services.json b/StudyBuddy/app/google-services.json
new file mode 100644
index 00000000..a242a565
--- /dev/null
+++ b/StudyBuddy/app/google-services.json
@@ -0,0 +1,29 @@
+{
+ "project_info": {
+ "project_number": "645370312545",
+ "project_id": "study-buddy-e11a2",
+ "storage_bucket": "study-buddy-e11a2.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:645370312545:android:f6abeceaf79f4a06373720",
+ "android_client_info": {
+ "package_name": "com.example.studybuddy"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCgeXGafBRoE84e32V3gdttHdjGay8LVn4"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/proguard-rules.pro b/StudyBuddy/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/StudyBuddy/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/StudyBuddy/app/src/androidTest/java/com/example/studybuddy/ExampleInstrumentedTest.kt b/StudyBuddy/app/src/androidTest/java/com/example/studybuddy/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..bfe46aec
--- /dev/null
+++ b/StudyBuddy/app/src/androidTest/java/com/example/studybuddy/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.studybuddy
+
+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("com.example.studybuddy", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/AndroidManifest.xml b/StudyBuddy/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..236cafa6
--- /dev/null
+++ b/StudyBuddy/app/src/main/AndroidManifest.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/MainActivity.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/MainActivity.kt
new file mode 100644
index 00000000..7869f2ad
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/MainActivity.kt
@@ -0,0 +1,105 @@
+package com.example.studybuddy
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.core.app.ActivityCompat
+import com.example.studybuddy.ui.theme.StudyBuddyTheme
+import com.example.studybuddy.view.NavGraphs
+import com.example.studybuddy.view.destinations.SessionScreenRouteDestination
+import com.example.studybuddy.view.session.StudySessionTimerService
+import com.ramcosta.composedestinations.DestinationsNavHost
+import com.ramcosta.composedestinations.navigation.dependency
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+
+ private var isBound by mutableStateOf(false)
+ private lateinit var timerService: StudySessionTimerService
+ private val connection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ val binder = service as StudySessionTimerService.StudySessionTimerBinder
+ timerService = binder.getService()
+ isBound = true
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ isBound = false
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Intent(this, StudySessionTimerService::class.java).also { intent ->
+ bindService(intent, connection, BIND_AUTO_CREATE)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ setContent {
+
+
+ // Only navigate when the service is bound
+ if (isBound) {
+ StudyBuddyTheme {
+ DestinationsNavHost(
+ navGraph = NavGraphs.root,
+ dependenciesContainerBuilder = {
+ dependency ( SessionScreenRouteDestination ) {timerService
+ }
+ }
+ )
+ }
+ } else {
+ // Show a loading UI or a message while the service is binding
+ LoadingScreen() // Custom composable to show a loading indicator
+ }
+ }
+
+
+ requestPermission()
+ }
+ private fun requestPermission(){
+ if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU)
+ {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
+ 0)
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ // Keep the service bound to maintain the session
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isBound) {
+ unbindService(connection)
+ isBound = false
+ }
+ }
+}
+
+@Composable
+fun LoadingScreen() {
+ // This is a placeholder UI to display while waiting for the service to bind
+ Text("Loading...")
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/StudyBuddyApp.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/StudyBuddyApp.kt
new file mode 100644
index 00000000..84b8149b
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/StudyBuddyApp.kt
@@ -0,0 +1,13 @@
+package com.example.studybuddy
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import java.util.UUID
+
+
+@HiltAndroidApp
+class StudyBuddyApp:Application(){
+ companion object{
+ val username = UUID.randomUUID().toString().substring(0,6)
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/AppModule.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/AppModule.kt
new file mode 100644
index 00000000..b8b438a4
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/AppModule.kt
@@ -0,0 +1,20 @@
+package com.example.studybuddy.domain.database.di
+
+import com.google.gson.Gson
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import android.content.Context
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+
+ @Provides
+ fun providesContext(@ApplicationContext context: Context):Context = context
+
+ @Provides
+ fun providesGson():Gson = Gson()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/DatabaseModule.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/DatabaseModule.kt
new file mode 100644
index 00000000..195a25f8
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/DatabaseModule.kt
@@ -0,0 +1,63 @@
+package com.example.studybuddy.domain.database.di
+
+import android.app.Application
+import androidx.room.Room
+import com.example.studybuddy.domain.database.local.AppDatabase
+import com.example.studybuddy.domain.database.local.SessionDao
+import com.example.studybuddy.domain.database.local.SubjectDao
+import com.example.studybuddy.domain.database.local.TaskDao
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DatabaseModule {
+
+
+@Provides
+@Singleton //it will make sure that there is only one instance of the provided function
+
+ fun provideDatabase(
+ application: Application
+ ):AppDatabase{
+ return Room.databaseBuilder(
+ application,
+ AppDatabase::class.java,
+ "studybuddy.db"
+ ).build()
+ }
+
+
+
+ @Provides
+ @Singleton
+
+ fun provideSubjectDao(database: AppDatabase) :SubjectDao{
+ return database.subjectDao()
+
+ }
+
+ @Provides
+ @Singleton
+
+ fun provideTaskDao(database: AppDatabase) : TaskDao {
+ return database.taskDao()
+
+ }
+
+ @Provides
+ @Singleton
+
+ fun provideSessionDao(database: AppDatabase) :SessionDao{
+ return database.sessionDao()
+
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/FirebaseModule.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/FirebaseModule.kt
new file mode 100644
index 00000000..b231cc93
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/FirebaseModule.kt
@@ -0,0 +1,30 @@
+package com.example.studybuddy.domain.database.di
+
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.firestore.FirebaseFirestore
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+
+@InstallIn(SingletonComponent::class)
+@Module
+object FirebaseModule {
+
+ @Provides
+ @Singleton
+ fun ProvideFirestoreInstance():FirebaseFirestore{
+ return FirebaseFirestore.getInstance()
+ }
+
+ @Provides
+ @Singleton
+ fun ProvideFirebaseAuthInstance():FirebaseAuth{
+ return FirebaseAuth.getInstance()
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/NotificationModule.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/NotificationModule.kt
new file mode 100644
index 00000000..35f016f1
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/NotificationModule.kt
@@ -0,0 +1,42 @@
+package com.example.studybuddy.domain.database.di
+
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import com.example.studybuddy.R
+import com.example.studybuddy.utils.ServiceConstants.NOTIFICATION_CHANNEL_ID
+import com.example.studybuddy.view.session.ServiceHelper
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ServiceComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ServiceScoped
+
+@Module
+@InstallIn(ServiceComponent::class)
+object NotificationModule {
+ @ServiceScoped
+ @Provides
+ fun provideNotificationBuilder(
+ @ApplicationContext context: Context
+
+ ): NotificationCompat.Builder{
+ return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
+ .setAutoCancel(false)
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.logo)
+ .setContentTitle("Study Session")
+ .setContentText("00:00:00")
+ .setContentIntent(ServiceHelper.clickPendingIntent(context))
+ }
+
+ @ServiceScoped
+ @Provides
+
+ fun provideNotificationManager(
+ @ApplicationContext context: Context
+ ): NotificationManager{
+ return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/RepositoryModule.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/RepositoryModule.kt
new file mode 100644
index 00000000..878ddef9
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/di/RepositoryModule.kt
@@ -0,0 +1,43 @@
+package com.example.studybuddy.domain.database.di
+
+import com.example.studybuddy.domain.database.repositoryImpl.SessionRepositoryImpl
+import com.example.studybuddy.domain.database.repositoryImpl.SubjectRepositoryImpl
+import com.example.studybuddy.domain.database.repositoryImpl.TaskRepositoryImpl
+import com.example.studybuddy.domain.model.repository.SessionRepository
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import com.example.studybuddy.domain.model.repository.TaskRepository
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class RepositoryModule {
+
+
+ @Singleton
+ @Binds
+ abstract fun bindSubjectRepository(
+ impl:SubjectRepositoryImpl
+ ):SubjectRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindTaskRepository(
+ impl: TaskRepositoryImpl
+ ): TaskRepository
+
+
+
+ @Singleton
+ @Binds
+ abstract fun bindSessionRepository(
+ impl: SessionRepositoryImpl
+ ): SessionRepository
+
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/AppDatabase.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/AppDatabase.kt
new file mode 100644
index 00000000..ede9861b
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/AppDatabase.kt
@@ -0,0 +1,22 @@
+package com.example.studybuddy.domain.database.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.utils.ColorListConverter
+
+
+@Database(entities = [Subject::class, Session::class, Task::class], version = 1)
+
+@TypeConverters(ColorListConverter::class)
+abstract class AppDatabase : RoomDatabase() {
+
+
+ abstract fun subjectDao(): SubjectDao
+ abstract fun sessionDao(): SessionDao
+ abstract fun taskDao(): TaskDao
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SessionDao.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SessionDao.kt
new file mode 100644
index 00000000..ec0463e1
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SessionDao.kt
@@ -0,0 +1,41 @@
+package com.example.studybuddy.domain.database.local
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import com.example.studybuddy.domain.model.Session
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface SessionDao {
+
+
+ @Insert
+ suspend fun insertSession(session: Session)
+
+ @Delete
+ suspend fun deleteSession(session: Session)
+
+
+ @Query("SELECT * FROM session")
+ fun getAllSessions(): Flow>
+
+
+ @Query("SELECT * FROM session WHERE sessionSubjectId = :subjectId")
+ fun getSessionBySubjectId(subjectId: Int): Flow>
+
+ @Query("SELECT SUM(duration) FROM Session")
+ fun getTotalSessionDuration(): Flow
+
+
+ @Query("SELECT SUM (duration) FROM session WHERE sessionSubjectId = :subjectId")
+ fun getTotalDurationBySubjectId(subjectId: Int): Flow
+
+ @Query("SELECT SUM (duration) FROM Session ")
+ fun getTotalDurationBySubjectId(): Flow
+
+
+ @Query("DELETE FROM session WHERE sessionSubjectId = :subjectId")
+ suspend fun deleteSessionBySubjectId(subjectId: Int)
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SubjectDao.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SubjectDao.kt
new file mode 100644
index 00000000..fe44baee
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/SubjectDao.kt
@@ -0,0 +1,38 @@
+package com.example.studybuddy.domain.database.local
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Upsert
+import com.example.studybuddy.domain.model.Subject
+import kotlinx.coroutines.flow.Flow
+
+
+@Dao
+interface SubjectDao {
+
+ @Upsert
+ suspend fun upsertSubject(subject: Subject)
+
+
+ @Query("SELECT COUNT(*) FROM SUBJECT")
+ fun getTotalSubjectCount(): Flow
+
+
+ @Query("SELECT SUM(goalHour) FROM SUBJECT")
+ fun getTotalGoalHours():Flow
+
+
+ @Query("SELECT * FROM SUBJECT WHERE subjectId = :subjectId")
+ suspend fun getSubjectById(subjectId : Int) : Subject?
+
+
+ @Query("DELETE FROM SUBJECT WHERE subjectId = :subjectId")
+ suspend fun deleteSubjectById(subjectId : Int)
+
+
+ @Query("SELECT * FROM SUBJECT")
+ fun getAllSubjects() : Flow>
+
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/TaskDao.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/TaskDao.kt
new file mode 100644
index 00000000..2182a6bb
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/local/TaskDao.kt
@@ -0,0 +1,33 @@
+package com.example.studybuddy.domain.database.local
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Upsert
+import com.example.studybuddy.domain.model.Task
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface TaskDao {
+
+ @Upsert
+ suspend fun upsertTask (task: Task)
+
+
+ @Query("DELETE FROM task WHERE taskId = :taskId")
+ suspend fun deleteTaskById(taskId: Int)
+
+
+ @Query("DELETE FROM task WHERE taskSubjectId = :subjectId")
+ suspend fun deleteTaskBySubjectId(subjectId: Int)
+
+
+ @Query("SELECT * FROM task WHERE taskId = :taskId")
+ suspend fun getTaskByTaskId(taskId: Int): Task?
+
+
+ @Query("SELECT * FROM task WHERE taskSubjectId = :subjectId")
+ fun getTaskBySubjectId(subjectId: Int): Flow>
+
+ @Query("SELECT * FROM task")
+ fun getAllTasks(): Flow>
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SessionRepositoryImpl.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SessionRepositoryImpl.kt
new file mode 100644
index 00000000..10e3b882
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SessionRepositoryImpl.kt
@@ -0,0 +1,52 @@
+package com.example.studybuddy.domain.database.repositoryImpl
+
+import com.example.studybuddy.domain.database.local.SessionDao
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.repository.SessionRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.take
+import javax.inject.Inject
+
+class SessionRepositoryImpl @Inject constructor(
+ private val sessionDao: SessionDao
+):SessionRepository {
+ override suspend fun insertSession(session: Session) {
+ sessionDao.insertSession(session)
+ }
+
+ override suspend fun deleteSession(session: Session) {
+ return sessionDao.deleteSession(session)
+ }
+
+ override fun getAllSessions(): Flow> {
+ return sessionDao.getAllSessions().map {
+ sessions-> sessions.sortedByDescending { it.date }
+ }
+ }
+
+
+ override fun getRecentFiveSessions(): Flow> {
+ return sessionDao.getAllSessions().map {
+ sessions-> sessions.sortedByDescending { it.date }
+ }
+ .take(5)
+ }
+
+
+ override suspend fun getSessionBySubjectId(subjectId: Int): Flow> {
+ TODO("Not yet implemented")
+ }
+
+ override fun getTotalSessionDuration(): Flow {
+ return sessionDao.getTotalSessionDuration()
+ }
+
+ override fun getTotalSessionDurationBySubjectId(subjectId: Int): Flow {
+ return sessionDao.getTotalDurationBySubjectId(subjectId)
+ }
+
+ override suspend fun deleteSessionBySubjectId(subjectId: Int) {
+ TODO("Not yet implemented")
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SubjectRepositoryImpl.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SubjectRepositoryImpl.kt
new file mode 100644
index 00000000..1c7c6e2f
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/SubjectRepositoryImpl.kt
@@ -0,0 +1,41 @@
+package com.example.studybuddy.domain.database.repositoryImpl
+
+import com.example.studybuddy.domain.database.local.SessionDao
+import com.example.studybuddy.domain.database.local.SubjectDao
+import com.example.studybuddy.domain.database.local.TaskDao
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class SubjectRepositoryImpl @Inject constructor(
+ private val subjectDao: SubjectDao,
+ private val taskDao: TaskDao,
+ private val sessionDao: SessionDao
+):SubjectRepository {
+ override suspend fun upsertSubject(subject: Subject) {
+ subjectDao.upsertSubject(subject)
+ }
+
+ override fun getTotalSubjectCount(): Flow {
+ return subjectDao.getTotalSubjectCount()
+ }
+
+ override fun getTotalGoalHours(): Flow {
+ return subjectDao.getTotalGoalHours()
+ }
+
+ override suspend fun getSubjectById(subjectId: Int): Subject? {
+ return subjectDao.getSubjectById(subjectId)
+ }
+
+ override suspend fun deleteSubjectById(subjectId: Int) {
+ taskDao.deleteTaskBySubjectId(subjectId)
+ sessionDao.deleteSessionBySubjectId(subjectId)
+ subjectDao.deleteSubjectById(subjectId)
+ }
+
+ override fun getAllSubjects(): Flow> {
+ return subjectDao.getAllSubjects()
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/TaskRepositoryImpl.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/TaskRepositoryImpl.kt
new file mode 100644
index 00000000..8e0b2ffa
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/database/repositoryImpl/TaskRepositoryImpl.kt
@@ -0,0 +1,53 @@
+package com.example.studybuddy.domain.database.repositoryImpl
+
+import com.example.studybuddy.domain.database.local.TaskDao
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.domain.model.repository.TaskRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class TaskRepositoryImpl @Inject constructor(
+ private val taskDao: TaskDao
+):TaskRepository {
+ override suspend fun upsertTask(task: Task) {
+ return taskDao.upsertTask(task)
+ }
+
+ override suspend fun deleteTaskById(taskId: Int) {
+ return taskDao.deleteTaskById(taskId)
+ }
+
+ override suspend fun deleteTaskBySubjectId(subjectId: Int) {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun getTaskByTaskId(taskId: Int): Task? {
+ return taskDao.getTaskByTaskId(taskId)
+ }
+
+ override fun getTaskBySubjectId(subjectId: Int): Flow> {
+ return taskDao.getTaskBySubjectId(subjectId)
+ .map { tasks -> tasks.filter { it.isCompleted.not() } }
+ }
+
+ override fun getAllTasks(): Flow> {
+ return taskDao.getAllTasks()
+ }
+
+ override fun getCompletedTaskBySubjectId(subjectId: Int): Flow> {
+ return taskDao.getTaskBySubjectId(subjectId)
+ .map { tasks -> tasks.filter { it.isCompleted} }
+ }
+
+ override fun getAllUpcomingTasks(): Flow> {
+ return taskDao.getAllTasks()
+ .map { tasks ->tasks.filter { it.isCompleted.not() } }
+ .map { tasks -> sortTasks(tasks) }
+ }
+
+private fun sortTasks(tasks: List): List {
+ return tasks.sortedWith(compareBy {it.dueDate }. thenByDescending { it.priority })
+}
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/BottomNavigationItem.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/BottomNavigationItem.kt
new file mode 100644
index 00000000..c299f42a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/BottomNavigationItem.kt
@@ -0,0 +1,9 @@
+package com.example.studybuddy.domain.model
+
+data class BottomNavigationItem(
+ val title:String,
+ val selectedIcon: Int,
+ val unselectedIcon:Int,
+ val hasNews:Boolean,
+ val badgeCount:Int? =null
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Chats.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Chats.kt
new file mode 100644
index 00000000..c1615553
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Chats.kt
@@ -0,0 +1,20 @@
+package com.example.studybuddy.domain.model
+
+data class ChatData(
+ val chatId: String? = "",
+ val user1:ChatUser = ChatUser(),
+ val user2:ChatUser = ChatUser(),
+)
+
+data class ChatUser(
+ val userId: String? = "",
+ val name: String? = "",
+ val phone: String? = "",
+)
+
+data class Message(
+ val userId: String="",
+ val time:String="",
+ val message:String=""
+)
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/MessageModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/MessageModel.kt
new file mode 100644
index 00000000..c9b46871
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/MessageModel.kt
@@ -0,0 +1,10 @@
+package com.example.studybuddy.domain.model
+
+import com.example.studybuddy.view.videocall.socket.SocketEvents
+
+data class MessageModel(
+ val type: SocketEvents,
+ val name: String? = null,
+ val target: String? = null,
+ val data:Any?=null
+)
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/RoomModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/RoomModel.kt
new file mode 100644
index 00000000..76d56e66
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/RoomModel.kt
@@ -0,0 +1,6 @@
+package com.example.studybuddy.domain.model
+
+data class RoomModel(
+ val roomName:String,
+ val population:Int
+)
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Session.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Session.kt
new file mode 100644
index 00000000..83444f7e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Session.kt
@@ -0,0 +1,16 @@
+package com.example.studybuddy.domain.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class Session(
+ val sessionSubjectId:Int,
+ @PrimaryKey(autoGenerate = true)
+ val sessionId:Int? = null,
+ val date:Long,
+ val duration:Long,
+ val relatedToSubject:String) {
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Subject.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Subject.kt
new file mode 100644
index 00000000..b41aaaa7
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Subject.kt
@@ -0,0 +1,24 @@
+package com.example.studybuddy.domain.model
+
+import androidx.compose.ui.graphics.Color
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.example.studybuddy.ui.theme.gradient1
+import com.example.studybuddy.ui.theme.gradient2
+import com.example.studybuddy.ui.theme.gradient3
+import com.example.studybuddy.ui.theme.gradient4
+import com.example.studybuddy.ui.theme.gradient5
+
+@Entity
+data class Subject(
+ val name: String,
+ val goalHour:Float,
+ val color:List,
+ @PrimaryKey(autoGenerate = true)
+ val SubjectId:Int? = null
+){
+ companion object{
+ val subjectCardColor = listOf(gradient1, gradient2, gradient3, gradient4, gradient5)
+
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Task.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Task.kt
new file mode 100644
index 00000000..97545493
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/Task.kt
@@ -0,0 +1,20 @@
+package com.example.studybuddy.domain.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+
+@Entity
+data class Task(
+ val title: String,
+ val description: String,
+ val dueDate:Long,
+ val dueTime:Long,
+ val setReminder:Boolean,
+ val priority:Int,
+ val relatedToSubject:String,
+ val isCompleted:Boolean,
+ @PrimaryKey(autoGenerate = true)
+ val TaskId: Int? = null,
+ val taskSubjectId:Int
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/User.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/User.kt
new file mode 100644
index 00000000..04c56575
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/User.kt
@@ -0,0 +1,18 @@
+package com.example.studybuddy.domain.model
+
+data class UserData(
+ val userId:String? = "",
+ val name: String? = "",
+ val number: String? = null,
+ val PremiumMember:Boolean? = true,
+ val expiry:Long? = null
+){
+ fun toMap() = mapOf(
+ "userId" to userId,
+ "userName" to name,
+ "number" to number,
+ "PremiumMember" to PremiumMember,
+ "Expiry" to expiry,
+
+ )
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SessionRepository.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SessionRepository.kt
new file mode 100644
index 00000000..a6864b71
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SessionRepository.kt
@@ -0,0 +1,26 @@
+package com.example.studybuddy.domain.model.repository
+
+import com.example.studybuddy.domain.model.Session
+import kotlinx.coroutines.flow.Flow
+
+interface SessionRepository {
+
+ suspend fun insertSession(session: Session)
+
+ suspend fun deleteSession(session: Session)
+
+ fun getAllSessions(): Flow>
+
+
+ fun getRecentFiveSessions(): Flow>
+
+ suspend fun getSessionBySubjectId(subjectId: Int): Flow>
+
+ fun getTotalSessionDuration(): Flow
+
+ fun getTotalSessionDurationBySubjectId(subjectId: Int): Flow
+
+ suspend fun deleteSessionBySubjectId(subjectId: Int)
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SubjectRepository.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SubjectRepository.kt
new file mode 100644
index 00000000..59df0ced
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/SubjectRepository.kt
@@ -0,0 +1,20 @@
+package com.example.studybuddy.domain.model.repository
+
+import com.example.studybuddy.domain.model.Subject
+import kotlinx.coroutines.flow.Flow
+
+interface SubjectRepository {
+
+ suspend fun upsertSubject(subject: Subject)
+
+ fun getTotalSubjectCount(): Flow
+
+ fun getTotalGoalHours(): Flow
+
+ suspend fun getSubjectById(subjectId : Int) : Subject?
+
+ suspend fun deleteSubjectById(subjectId : Int)
+
+ fun getAllSubjects() : Flow>
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/TaskRepository.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/TaskRepository.kt
new file mode 100644
index 00000000..6b741477
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/domain/model/repository/TaskRepository.kt
@@ -0,0 +1,27 @@
+package com.example.studybuddy.domain.model.repository
+
+import com.example.studybuddy.domain.model.Task
+import kotlinx.coroutines.flow.Flow
+
+interface TaskRepository {
+
+ suspend fun upsertTask (task: Task)
+
+
+ suspend fun deleteTaskById(taskId: Int)
+
+
+ suspend fun deleteTaskBySubjectId(subjectId: Int)
+
+
+ suspend fun getTaskByTaskId(taskId: Int): Task?
+
+
+ fun getTaskBySubjectId(subjectId: Int): Flow>
+
+ fun getCompletedTaskBySubjectId(subjectId: Int): Flow>
+
+ fun getAllTasks(): Flow>
+
+ fun getAllUpcomingTasks(): Flow>
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Color.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Color.kt
new file mode 100644
index 00000000..10db784a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Color.kt
@@ -0,0 +1,75 @@
+package com.example.studybuddy.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val primaryLight = Color(0xFF455E91)
+val onPrimaryLight = Color(0xFFFFFFFF)
+val primaryContainerLight = Color(0xFFD8E2FF)
+val onPrimaryContainerLight = Color(0xFF001A43)
+val secondaryLight = Color(0xFF575E71)
+val onSecondaryLight = Color(0xFFFFFFFF)
+val secondaryContainerLight = Color(0xFFDBE2F9)
+val onSecondaryContainerLight = Color(0xFF141B2C)
+val tertiaryLight = Color(0xFF715573)
+val onTertiaryLight = Color(0xFFFFFFFF)
+val tertiaryContainerLight = Color(0xFFFCD7FB)
+val onTertiaryContainerLight = Color(0xFF2A132D)
+val errorLight = Color(0xFFBA1A1A)
+val onErrorLight = Color(0xFFFFFFFF)
+val errorContainerLight = Color(0xFFFFDAD6)
+val onErrorContainerLight = Color(0xFF410002)
+val backgroundLight = Color(0xFFFAF9FF)
+val onBackgroundLight = Color(0xFF1A1B20)
+val surfaceLight = Color(0xFFFAF9FF)
+val onSurfaceLight = Color(0xFF1A1B20)
+val surfaceVariantLight = Color(0xFFE1E2EC)
+val onSurfaceVariantLight = Color(0xFF44474F)
+val outlineLight = Color(0xFF757780)
+val outlineVariantLight = Color(0xFFC5C6D0)
+val scrimLight = Color(0xFF000000)
+val inverseSurfaceLight = Color(0xFF2F3036)
+val inverseOnSurfaceLight = Color(0xFFF1F0F7)
+val inversePrimaryLight = Color(0xFFAEC6FF)
+
+
+val primaryDark = Color(0xFFAEC6FF)
+val onPrimaryDark = Color(0xFF132F60)
+val primaryContainerDark = Color(0xFF2D4678)
+val onPrimaryContainerDark = Color(0xFFD8E2FF)
+val secondaryDark = Color(0xFFBFC6DC)
+val onSecondaryDark = Color(0xFF293041)
+val secondaryContainerDark = Color(0xFF3F4759)
+val onSecondaryContainerDark = Color(0xFFDBE2F9)
+val tertiaryDark = Color(0xFFDFBBDE)
+val onTertiaryDark = Color(0xFF402843)
+val tertiaryContainerDark = Color(0xFF583E5A)
+val onTertiaryContainerDark = Color(0xFFFCD7FB)
+val errorDark = Color(0xFFFFB4AB)
+val onErrorDark = Color(0xFF690005)
+val errorContainerDark = Color(0xFF93000A)
+val onErrorContainerDark = Color(0xFFFFDAD6)
+val backgroundDark = Color(0xFF121318)
+val onBackgroundDark = Color(0xFFE2E2E9)
+val surfaceDark = Color(0xFF121318)
+val onSurfaceDark = Color(0xFFE2E2E9)
+val surfaceVariantDark = Color(0xFF44474F)
+val onSurfaceVariantDark = Color(0xFFC5C6D0)
+val outlineDark = Color(0xFF8E9099)
+val outlineVariantDark = Color(0xFF44474F)
+val scrimDark = Color(0xFF000000)
+val inverseSurfaceDark = Color(0xFFE2E2E9)
+val inverseOnSurfaceDark = Color(0xFF2F3036)
+val inversePrimaryDark = Color(0xFF455E91)
+
+val Red = Color(0xFFD53A2F)
+val BlueText = Color(0xFF455E91)
+val Green = Color(0xFF1E9651)
+val Orange = Color(0xFFFF9800)
+val grey = Color(0xFFEBF5FF)
+val buttonColor = Color(0xFF006782)
+
+val gradient1 = listOf(Color(0xFFad5389), Color(0xFF3c1053))
+val gradient2 = listOf(Color(0xFF3a6073), Color(0xFF16222a))
+val gradient3 = listOf(Color(0xFFF857a6), Color(0xFFff5858))
+val gradient4 = listOf(Color(0xFF00d2ff), Color(0xFF3a7bd5))
+val gradient5 = listOf(Color(0xFF99f2c8), Color(0xFF1f4037))
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Theme.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Theme.kt
new file mode 100644
index 00000000..4200de6f
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Theme.kt
@@ -0,0 +1,104 @@
+package com.example.studybuddy.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+
+private val LightColorScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ primaryContainer = primaryContainerLight,
+ onPrimaryContainer = onPrimaryContainerLight,
+ secondary = secondaryLight,
+ onSecondary = onSecondaryLight,
+ secondaryContainer = secondaryContainerLight,
+ onSecondaryContainer = onSecondaryContainerLight,
+ tertiary = tertiaryLight,
+ onTertiary = onTertiaryLight,
+ tertiaryContainer = tertiaryContainerLight,
+ onTertiaryContainer = onTertiaryContainerLight,
+ error = errorLight,
+ onError = onErrorLight,
+ errorContainer = errorContainerLight,
+ onErrorContainer = onErrorContainerLight,
+ background = backgroundLight,
+ onBackground = onBackgroundLight,
+ surface = surfaceLight,
+ onSurface = onSurfaceLight,
+ surfaceVariant = surfaceVariantLight,
+ onSurfaceVariant = onSurfaceVariantLight,
+ outline = outlineLight,
+ outlineVariant = outlineVariantLight,
+ scrim = scrimLight,
+ inverseSurface = inverseSurfaceLight,
+ inverseOnSurface = inverseOnSurfaceLight,
+ inversePrimary = inversePrimaryLight,
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ onTertiaryContainer = onTertiaryContainerDark,
+ error = errorDark,
+ onError = onErrorDark,
+ errorContainer = errorContainerDark,
+ onErrorContainer = onErrorContainerDark,
+ background = backgroundDark,
+ onBackground = onBackgroundDark,
+ surface = surfaceDark,
+ onSurface = onSurfaceDark,
+ surfaceVariant = surfaceVariantDark,
+ onSurfaceVariant = onSurfaceVariantDark,
+ outline = outlineDark,
+ outlineVariant = outlineVariantDark,
+ scrim = scrimDark,
+ inverseSurface = inverseSurfaceDark,
+ inverseOnSurface = inverseOnSurfaceDark,
+ inversePrimary = inversePrimaryDark,
+)
+
+
+
+@Composable
+fun StudyBuddyTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ 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/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Type.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Type.kt
new file mode 100644
index 00000000..e28d17bc
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/ui/theme/Type.kt
@@ -0,0 +1,133 @@
+package com.example.studybuddy.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.googlefonts.Font
+import androidx.compose.ui.text.googlefonts.GoogleFont
+import androidx.compose.ui.unit.sp
+import com.example.studybuddy.R
+
+val provider = GoogleFont.Provider(
+ providerAuthority = "com.google.android.gms.fonts",
+ providerPackage = "com.google.android.gms",
+ certificates = R.array.com_google_android_gms_fonts_certs
+)
+
+val ubuntuFontFamily = FontFamily(
+ Font(
+ googleFont = GoogleFont("Ubuntu"),
+ fontProvider = provider
+ )
+)
+
+val salsaFontFamily = FontFamily(
+ Font(
+ googleFont = GoogleFont("Salsa"),
+ fontProvider = provider
+ )
+)
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 57.sp,
+ lineHeight = 64.sp
+ ),
+ displayMedium = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 45.sp,
+ lineHeight = 52.sp
+ ),
+ displaySmall = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 36.sp,
+ lineHeight = 44.sp
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 32.sp,
+ lineHeight = 40.sp
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 28.sp,
+ lineHeight = 36.sp
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = salsaFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 24.sp,
+ lineHeight = 32.sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.5.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ titleSmall = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.5.sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.5.sp
+ ),
+ bodySmall = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelMedium = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = ubuntuFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+)
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ColorListConverter.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ColorListConverter.kt
new file mode 100644
index 00000000..708c6cc9
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ColorListConverter.kt
@@ -0,0 +1,17 @@
+package com.example.studybuddy.utils
+
+import androidx.room.TypeConverter
+
+class ColorListConverter {
+
+ @TypeConverter
+ fun fromColorList(colorList: List): String {
+ return colorList.joinToString(","){it.toString()}
+ }
+
+ @TypeConverter
+ fun toColorList(colorString: String): List {
+ return colorString.split(",").map { it.toInt() }
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Constants.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Constants.kt
new file mode 100644
index 00000000..d3711f2f
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Constants.kt
@@ -0,0 +1,5 @@
+package com.example.studybuddy.utils
+
+const val Chat_Data = "chats"
+const val User_Node = "user"
+const val MESSAGE ="message"
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Priority.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Priority.kt
new file mode 100644
index 00000000..5450713d
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/Priority.kt
@@ -0,0 +1,78 @@
+package com.example.studybuddy.utils
+
+import android.util.Log
+import androidx.compose.material3.SnackbarDuration
+import com.example.studybuddy.ui.theme.Green
+import com.example.studybuddy.ui.theme.Orange
+import com.example.studybuddy.ui.theme.Red
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.util.Date
+import java.util.Locale
+
+enum class Priority(val title: String, val color:androidx.compose.ui.graphics.Color, val value: Int) {
+ LOW(title = "Low", color = Green, value = 1),
+ MEDIUM(title = "Medium", color = Orange, value = 2),
+ HIGH(title = "High", color = Red, value = 3);
+
+
+ companion object {
+ fun fromInt(value: Int) = values().firstOrNull { it.value == value }?:MEDIUM
+ }
+
+
+}
+
+
+fun Long?.changeMillisToDateString():String{
+ val date:LocalDate = this?.let {
+ Instant
+ .ofEpochMilli(it)
+ .atZone(java.time.ZoneId.systemDefault())
+ .toLocalDate()
+
+ }?: LocalDate.now()
+
+ return date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
+}
+
+fun millisToTimeString(millis: Long?, pattern: String = "hh:mm a"): String {
+ return millis?.let {
+ val formatter = SimpleDateFormat(pattern, Locale.getDefault())
+ formatter.format(Date(it))
+ } ?: "No Time Set"
+}
+
+
+
+fun Long.toHours():Float{
+ val second = this.toFloat()
+ val hours = second.div(3600)
+ return "%.2f".format(hours).toFloat()
+}
+
+
+
+
+sealed class SnackbarEvent{
+ data class ShowSnackbar(
+ val message:String,
+ val duration: SnackbarDuration = SnackbarDuration.Short
+ ):SnackbarEvent()
+
+ data object NavigateUp:SnackbarEvent()
+
+ fun Int.TimeToString():String{
+ return this.toString().padStart(length = 2, padChar = '0')
+ }
+}
+
+fun handleException(tag: String, message: String, exception: Exception?) {
+ // Log the error
+ Log.e(tag, message, exception)
+
+ // Optionally, you can show a snackbar or toast
+ SnackbarEvent.ShowSnackbar("Error: ${exception?.localizedMessage ?: "Unknown error"}")
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ServiceConstants.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ServiceConstants.kt
new file mode 100644
index 00000000..d89a3b51
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/utils/ServiceConstants.kt
@@ -0,0 +1,14 @@
+package com.example.studybuddy.utils
+
+object ServiceConstants {
+
+ const val Action_Service_Start = "ACTION_SERVICE_START"
+ const val Action_Service_Stop = "ACTION_SERVICE_STOP"
+ const val Action_Service_Cancel = "ACTION_SERVICE_CANCEL"
+
+ const val NOTIFICATION_CHANNEL_ID = "TIMER_NOTIFICATION_ID"
+ const val NOTIFICATION_CHANNEL_NAME = "TIMER_NOTIFICATION"
+ const val NOTIFICATION_ID = 10
+
+ const val CLICK_REQUEST_CODE = 100
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/FocusMode/FocusScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/FocusMode/FocusScreen.kt
new file mode 100644
index 00000000..2bde6d1e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/FocusMode/FocusScreen.kt
@@ -0,0 +1,198 @@
+package com.example.studybuddy.view.FocusMode
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.studybuddy.R
+import com.example.studybuddy.ui.theme.grey
+import com.example.studybuddy.view.components.CustomToggleButton
+
+
+@Composable
+fun FocusScreen() {
+ val (blockYoutube, setBlockYoutube) = remember { mutableStateOf(false) }
+ val (blockInstagram, setBlockInstagram) = remember { mutableStateOf(false) }
+ val (blockWhatsapp, setBlockWhatsapp) = remember { mutableStateOf(false) }
+
+ var showConfirmDialog by remember { mutableStateOf Unit>?>(null) }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = { FocusTopBar() }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier.padding(paddingValues),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Do you want to block any social media app?",
+ style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.W500)
+ )
+ AppBlockCard(
+ appName = "YouTube",
+ isBlocked = blockYoutube,
+ onToggle = {
+ if (!blockYoutube) { // Show confirmation dialog only when toggling on
+ showConfirmDialog = "YouTube" to { setBlockYoutube(!blockYoutube) }
+ } else {
+ setBlockYoutube(!blockYoutube) // Directly toggle off
+ }
+ }
+ )
+ AppBlockCard(
+ appName = "Instagram",
+ isBlocked = blockInstagram,
+ onToggle = {
+ if (!blockInstagram) {
+ showConfirmDialog = "Instagram" to { setBlockInstagram(!blockInstagram) }
+ } else {
+ setBlockInstagram(!blockInstagram)
+ }
+ }
+ )
+ AppBlockCard(
+ appName = "WhatsApp",
+ isBlocked = blockWhatsapp,
+ onToggle = {
+ if (!blockWhatsapp) {
+ showConfirmDialog = "WhatsApp" to { setBlockWhatsapp(!blockWhatsapp) }
+ } else {
+ setBlockWhatsapp(!blockWhatsapp)
+ }
+ }
+ )
+ }
+
+ showConfirmDialog?.let { (appName, onConfirm) ->
+ ConfirmBlockDialog(
+ appName = appName,
+ onConfirm = {
+ onConfirm()
+ showConfirmDialog = null
+ },
+ onDismiss = { showConfirmDialog = null }
+ )
+ }
+ }
+}
+
+
+@Composable
+fun AppBlockCard(
+ appName: String,
+ isBlocked: Boolean,
+ onToggle: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp),
+ colors = CardDefaults.cardColors(grey),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Image(
+ painter = when (appName) {
+ "YouTube" -> painterResource(R.drawable.youtube)
+ "Instagram" -> painterResource(R.drawable.instagram)
+ "WhatsApp" -> painterResource(R.drawable.whatsapp)
+ else -> painterResource(R.drawable.youtube)//todo change this
+ },
+ contentDescription = "Focus",
+ modifier = Modifier.size(50.dp)
+ )
+ Text(
+ text = appName,
+ modifier = Modifier
+ .padding(horizontal = 32.dp)
+ .align(Alignment.CenterVertically),
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+ CustomToggleButton(
+ checked = isBlocked,
+ onCheckedClick = { onToggle() }
+ )
+ }
+ }
+}
+
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FocusTopBar() {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ text = "Focus Mode",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
+ )
+}
+
+@Composable
+fun ConfirmBlockDialog(
+ appName: String,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ androidx.compose.material3.AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(text = "Block $appName?") },
+ text = { Text(text = "Are you sure you want to block $appName?") },
+ confirmButton = {
+ androidx.compose.material3.TextButton(onClick = onConfirm) {
+ Text("Confirm")
+ }
+ },
+ dismissButton = {
+ androidx.compose.material3.TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ )
+}
+
+
+
+@Preview
+@Composable
+fun FocusScreenPreview(){
+ FocusScreen()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/MainScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/MainScreen.kt
new file mode 100644
index 00000000..8ab89401
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/MainScreen.kt
@@ -0,0 +1,131 @@
+package com.example.studybuddy.view
+
+import android.content.Intent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import com.example.studybuddy.R
+import com.example.studybuddy.domain.model.BottomNavigationItem
+import com.example.studybuddy.view.chat.ChatListScreen
+import com.example.studybuddy.view.chat.ChatListViewModel
+import com.example.studybuddy.view.dashboard.DashBoardScreenRoute
+import com.example.studybuddy.view.videocall.HomeScreen
+import com.example.studybuddy.view.videocall.HomeScreenRoute
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+
+@com.ramcosta.composedestinations.annotation.Destination(
+ deepLinks = [
+ DeepLink(
+ action = Intent.ACTION_VIEW,
+ uriPattern = "study_buddy://main"
+ )
+ ]
+)
+
+@Composable
+fun MainScreenRoute(
+ navigator: DestinationsNavigator,
+ initialSelectedIndex: Int = 0 // Default to 0 (Dashboard)
+) {
+ MainScreen(
+ modifier = Modifier,
+ navigator = navigator,
+ initialSelectedIndex = initialSelectedIndex
+ )
+}
+
+@Composable
+fun MainScreen(
+ modifier: Modifier = Modifier,
+ navigator: DestinationsNavigator,
+ initialSelectedIndex: Int
+) {
+ val navigationItems = listOf(
+ BottomNavigationItem(
+ title = "Dashboard",
+ selectedIcon = R.drawable.homefilled,
+ unselectedIcon = R.drawable.homeoutlined,
+ hasNews = false
+ ),
+ BottomNavigationItem(
+ title = "Chats",
+ selectedIcon = R.drawable.chatfilled,
+ unselectedIcon = R.drawable.chatoutlined,
+ hasNews = false
+ ),
+ BottomNavigationItem(
+ title = "Calls",
+ selectedIcon = R.drawable.videocallfill,
+ unselectedIcon = R.drawable.videocalloutline,
+ hasNews = false
+ ),
+// BottomNavigationItem(
+// title = "Focus",
+// selectedIcon = R.drawable.focusfill,
+// unselectedIcon = R.drawable.focus,
+// hasNews = false
+// )
+ )
+
+ var selectedIndex by remember { mutableStateOf(initialSelectedIndex) }
+
+ Scaffold(
+ bottomBar = {
+ BottomNavigationBar(
+ items = navigationItems,
+ selectedIndex = selectedIndex,
+ onItemSelected = { selectedIndex = it }
+ )
+ }
+ ) { paddingValues ->
+ ContentScreen(
+ navigator = navigator,
+ modifier = Modifier.padding(paddingValues),
+ selectedIndex = selectedIndex
+ )
+ }
+}
+
+
+@Composable
+fun BottomNavigationBar(items: List, selectedIndex: Int, onItemSelected: (Int) -> Unit) {
+ NavigationBar {
+ items.forEachIndexed { index, item ->
+ NavigationBarItem(
+ selected = selectedIndex == index,
+ onClick = { onItemSelected(index) },
+ icon = {
+ Icon(
+ painter = painterResource(id = if (selectedIndex == index) item.selectedIcon else item.unselectedIcon),
+ contentDescription = item.title
+ )
+ },
+ label = { Text(item.title) },
+ alwaysShowLabel = true // Show label always or only when selected
+ )
+ }
+ }
+}
+
+@Composable
+fun ContentScreen(navigator: DestinationsNavigator,modifier: Modifier = Modifier,selectedIndex: Int)
+{
+ when (selectedIndex) {
+ 0 -> DashBoardScreenRoute(navigator)
+ 1 -> ChatListScreen(navigator)
+ 2 -> HomeScreenRoute(navigator)
+// 3 -> Text("Doubts Screen", modifier = modifier.fillMaxSize())
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/AuthViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/AuthViewModel.kt
new file mode 100644
index 00000000..c67428e8
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/AuthViewModel.kt
@@ -0,0 +1,157 @@
+package com.example.studybuddy.view.auth
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.example.studybuddy.domain.model.UserData
+import com.example.studybuddy.utils.User_Node
+import com.example.studybuddy.utils.handleException
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.firestore.FirebaseFirestore
+import com.google.firebase.firestore.toObject
+
+class AuthViewModel : ViewModel() {
+ private val auth: FirebaseAuth = FirebaseAuth.getInstance()
+ private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance()
+
+ val _authState = MutableLiveData()
+ val authState:LiveData = _authState
+ var inProcess = mutableStateOf(false)
+ val userData = mutableStateOf(null)
+
+
+ init {
+ checkAuthStatus()
+ }
+
+ fun checkAuthStatus(){
+ if (auth.currentUser!=null)
+ {
+ _authState.value = AuthState.Authenticated
+ }
+ else{
+ _authState.value=AuthState.Unauthenticated
+ }
+ }
+
+ fun login(email:String, password:String)
+ {
+ if (email.isEmpty()||password.isEmpty())
+ {
+ _authState.value = AuthState.Error("Please fill all the credentials before login")
+ return
+ }
+ _authState.value = AuthState.Loading
+ auth.signInWithEmailAndPassword(email,password)
+ .addOnCompleteListener {task ->
+ if (task.isSuccessful)
+ {
+ _authState.value = AuthState.Authenticated
+ }else{
+ _authState.value = AuthState.Error(task.exception?.message?:"Something went wrong")
+ }
+ }
+ }
+
+ fun signUp(email: String, password: String, name: String, premiumMember: Boolean,number: String?,expiry: Long?) {
+ if (email.isEmpty() || password.isEmpty() || name.isEmpty()) {
+
+ _authState.value = AuthState.Error("Please fill all the credentials before signing up")
+ return
+ }
+
+ inProcess.value=true
+
+ firestore.collection(User_Node).whereEqualTo("number" , number).get().addOnSuccessListener {
+ if (it.isEmpty){
+ auth.createUserWithEmailAndPassword(email, password)
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ createUser(name,number,premiumMember, expiry)
+ } else {
+ _authState.value = AuthState.Error(task.exception?.message ?: "Something went wrong")
+ }
+ }
+ }
+ else{
+ handleException("AuthViewModel","Phone number already exist",null)
+ inProcess.value = false
+ }
+ }
+
+ }
+
+
+
+ fun createUser(name:String? = null, number:String? = null,premiumMember: Boolean? = null,expiry:Long? =null){
+ var uid = auth.currentUser?.uid
+ val userData = UserData(
+ userId = uid,
+ name = name?: userData.value?.name,
+ number = number?: userData.value?.number,
+ PremiumMember = premiumMember?:userData.value?.PremiumMember,
+ expiry = expiry?: userData.value?.expiry
+ )
+ uid?.let {
+ inProcess.value = true
+
+ firestore.collection(User_Node).document(uid).get().addOnSuccessListener {
+ if (it.exists()){
+ handleException("AuthViewModel","User already exist",null)
+ inProcess.value = false
+ getUserData(uid)
+
+ }
+
+ else{
+ firestore.collection(User_Node).document(uid).set(userData)
+ inProcess.value = false
+ }
+ }.addOnFailureListener {
+ handleException("AuthViewModel","User can't retrieved",null)
+ }
+
+ }
+
+
+ }
+
+ private fun getUserData(uid:String) {
+ inProcess.value = true
+ firestore.collection(User_Node).document(uid).addSnapshotListener{
+ value, error->
+ if (error!=null)
+ {
+ handleException("AuthViewModel","Something went wrong",error)
+ }
+ if (value != null){
+ var user = value.toObject()
+ userData.value = user
+ inProcess.value = false
+ }
+ }
+ }
+
+ fun logout(){
+ auth.signOut()
+ _authState.value = AuthState.Unauthenticated
+ }
+
+ fun checkSubscriptionStatus(onResult: (Boolean) -> Unit) {
+
+ }
+
+
+
+}
+
+
+sealed class AuthState{
+ data object Authenticated: AuthState()
+ data object Unauthenticated: AuthState()
+ data object Loading: AuthState()
+
+ data object RegisterSuccess: AuthState()
+ data class Error(val message: String): AuthState()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/SignupScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/SignupScreen.kt
new file mode 100644
index 00000000..966382b2
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/SignupScreen.kt
@@ -0,0 +1,349 @@
+package com.example.studybuddy.view.auth
+
+import android.content.Intent
+import android.widget.Toast
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+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.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+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.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.R
+import com.example.studybuddy.ui.theme.BlueText
+import com.example.studybuddy.ui.theme.buttonColor
+import com.example.studybuddy.ui.theme.grey
+import com.example.studybuddy.view.destinations.LoginScreenRouteDestination
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+
+@com.ramcosta.composedestinations.annotation.Destination(
+ deepLinks = [
+ DeepLink(action = Intent.ACTION_VIEW,
+ uriPattern = "study_buddy://signup")
+ ]
+)
+@Composable
+fun SignupScreenRoute(
+ navigator: DestinationsNavigator,
+) {
+ val viewModel: AuthViewModel = hiltViewModel()
+ SignupScreen(
+ viewModel = viewModel,
+ onLoginClick = { navigator.navigateUp() },
+ onSignupSuccess = { navigator.navigate(LoginScreenRouteDestination) }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SignupScreen(
+ viewModel: AuthViewModel,
+ onLoginClick: () -> Unit,
+ onSignupSuccess: () -> Unit
+) {
+ var userName by remember { mutableStateOf("") }
+ var userPhone by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var confirmPassword by remember { mutableStateOf("") }
+ var userEmail by remember { mutableStateOf("") }
+ val authState by viewModel.authState.observeAsState()
+ val context = LocalContext.current
+ var passwordVisible by remember { mutableStateOf(false) }
+ var confirmPasswordVisible by remember { mutableStateOf(false) }
+
+ LaunchedEffect(authState) {
+ when (authState) {
+ is AuthState.Loading -> {
+ // Show loading indicator
+ }
+ is AuthState.Error -> {
+ val message = (authState as AuthState.Error).message
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ }
+ is AuthState.Authenticated -> {
+ onSignupSuccess()
+ }
+ else -> Unit
+ }
+ }
+
+ Scaffold { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = 12.dp)
+ ) {
+ item {
+ Image(
+ painter = painterResource(id = R.drawable.signup),
+ contentDescription = "Signup Image",
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .fillMaxWidth()
+ .size(250.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Box(modifier = Modifier.fillMaxWidth()) {
+ Text(
+ text = "Sign up",
+ style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Column(modifier = Modifier.padding(horizontal = 25.dp)) {
+ Text(
+ text = "Name",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ TextField(
+ value = userName,
+ onValueChange = { userName = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ placeholder = {
+ Text("Enter your Name")
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "Mobile Number",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ TextField(
+ value = userPhone,
+ onValueChange = {
+ userPhone = it},
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true,
+ placeholder = {
+ Text("Enter your Phone Number")
+ }
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Email",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+
+ TextField(
+ value = userEmail,
+ onValueChange = { userEmail = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ placeholder = {
+ Text("Enter your Email")
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Password",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ placeholder = {
+ Text("Enter your password")
+ },
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ val icon = if (passwordVisible)
+ painterResource(id = R.drawable.baseline_visibility_24)
+ else
+ painterResource(id = R.drawable.baseline_visibility_off_24)
+
+ IconButton(onClick = {
+ passwordVisible = !passwordVisible
+ }) {
+ Image(
+ painter = icon,
+ contentDescription = if (passwordVisible) "Hide password" else "Show password"
+ )
+ }
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "Confirm Password",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+
+ TextField(
+ value = confirmPassword,
+ onValueChange = { confirmPassword = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ placeholder = {
+ Text("Re-enter your password")
+ },
+ visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ val icon = if (confirmPasswordVisible)
+ painterResource(id = R.drawable.baseline_visibility_24)
+ else
+ painterResource(id = R.drawable.baseline_visibility_off_24)
+
+ IconButton(onClick = {
+ confirmPasswordVisible = !confirmPasswordVisible
+ }) {
+ Image(
+ painter = icon,
+ contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password"
+ )
+ }
+ }
+ )
+
+
+ Spacer(modifier = Modifier.height(15.dp))
+
+ Button(
+ onClick = {
+ if (userEmail.isNotBlank() && isValidEmail(userEmail) &&
+ password.isNotBlank() && confirmPassword.isNotBlank() &&
+ password == confirmPassword && userName.isNotBlank() && userPhone.isNotBlank()
+ ) {
+ viewModel.signUp(
+ name = userName,
+ number = userPhone,
+ premiumMember = false,
+ expiry = 0,
+ email = userEmail,
+ password = password
+ )
+ userName = ""
+ userEmail = ""
+ password = ""
+ confirmPassword = ""
+ userPhone = ""
+ } else {
+ if (password != confirmPassword) {
+ viewModel._authState.value = AuthState.Error("Passwords do not match")
+ } else if (!isValidEmail(userEmail)) {
+ viewModel._authState.value = AuthState.Error("Please fill a valid email")
+ } else {
+ viewModel._authState.value = AuthState.Error("Please fill all fields correctly")
+ }
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 48.dp, vertical = 20.dp),
+ colors = ButtonDefaults.buttonColors(buttonColor),
+ enabled = viewModel._authState.value != AuthState.Loading
+ ) {
+ Text(text = "Sign up")
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
+ Text(
+ text = "Have an account?",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = "Log in",
+ style = MaterialTheme.typography.bodyMedium.copy(color = BlueText),
+ modifier = Modifier.clickable { onLoginClick() }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(30.dp))
+ }
+ }
+ }
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/loginScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/loginScreen.kt
new file mode 100644
index 00000000..46d63f8b
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/auth/loginScreen.kt
@@ -0,0 +1,254 @@
+package com.example.studybuddy.view.auth
+
+import android.content.Intent
+import android.widget.Toast
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+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.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+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.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.R
+import com.example.studybuddy.ui.theme.BlueText
+import com.example.studybuddy.ui.theme.buttonColor
+import com.example.studybuddy.ui.theme.grey
+import com.example.studybuddy.view.destinations.MainScreenRouteDestination
+import com.example.studybuddy.view.destinations.SignupScreenRouteDestination
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+
+@com.ramcosta.composedestinations.annotation.Destination(
+ deepLinks = [
+ DeepLink(
+ action = Intent.ACTION_VIEW,
+ uriPattern = "study_buddy://login")
+ ]
+)
+@Composable
+fun LoginScreenRoute(
+ navigator: DestinationsNavigator,
+
+) {
+ val viewModel: AuthViewModel = hiltViewModel()
+ val authState by viewModel.authState.observeAsState()
+
+
+ LoginScreen(
+ viewModel = viewModel,
+ onLoginSuccess = {
+ navigator.navigate(MainScreenRouteDestination(0))
+ },
+ signupisclicked = {
+ navigator.navigate(SignupScreenRouteDestination)
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LoginScreen(
+ viewModel: AuthViewModel,
+ onLoginSuccess: () -> Unit,
+ signupisclicked: () -> Unit = {}
+
+) {
+ var password by remember {
+ mutableStateOf("")
+ }
+ var userEmail by remember {
+ mutableStateOf("")
+ }
+ var passwordVisible by remember { mutableStateOf(false) }
+val authState by viewModel.authState.observeAsState()
+ val context = LocalContext.current
+
+ LaunchedEffect(authState) {
+ when (authState) {
+
+ is AuthState.Error -> {
+ val message = (authState as AuthState.Error).message
+ Toast.makeText(context,message, Toast.LENGTH_SHORT).show()
+ }
+ is AuthState.Authenticated -> {
+ onLoginSuccess()
+ }
+
+ else -> Unit
+ }
+ }
+
+ Scaffold { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = 12.dp)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.boyimg),
+ contentDescription = "Login Image",
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .fillMaxWidth()
+ .size(250.dp)
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = "Log in",
+ style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Column(modifier = Modifier.padding(25.dp)) {
+ Text(
+ text = "Email",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+
+ TextField(
+ value = userEmail,
+ onValueChange = { userEmail = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = "Password",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.W500)
+ )
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ singleLine = true,
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ val icon = if (passwordVisible)
+ painterResource(id = R.drawable.baseline_visibility_24)
+ else
+ painterResource(id = R.drawable.baseline_visibility_off_24)
+
+ IconButton(onClick = {
+ passwordVisible = !passwordVisible
+ }) {
+ Image(
+ painter = icon,
+ contentDescription = if (passwordVisible) "Hide password" else "Show password"
+ )
+ }
+ }
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Button(
+ onClick = {
+ if (userEmail.isNotBlank() && password.isNotBlank()&& isValidEmail(userEmail)) {
+ viewModel.login(
+ email = userEmail,
+ password = password
+ )
+ } else {
+ if (isValidEmail(userEmail)==false)
+ {
+ viewModel._authState.value = AuthState.Error("Please fill valid email")
+
+ }
+ else{
+ viewModel._authState.value = AuthState.Error("Please enter both email and password")
+ }
+
+ }
+ },
+ enabled = viewModel._authState.value != AuthState.Loading ,
+
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 48.dp, vertical = 20.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = buttonColor // Set the custom color here
+ )
+ ) {
+ Text(text = "Log in")
+ }
+
+ Spacer(modifier = Modifier.height(50.dp))
+
+ Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
+ Text(
+ text = "No Account?",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = "Sign up",
+ style = MaterialTheme.typography.bodyMedium.copy(color = BlueText),
+ modifier = Modifier.clickable { signupisclicked()}
+ )
+ }
+
+
+ }
+ }
+ }
+}
+
+fun isValidEmail(email: String): Boolean {
+ val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$".toRegex()
+ return email.matches(emailRegex)
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatActivity.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatActivity.kt
new file mode 100644
index 00000000..0dafb1ca
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatActivity.kt
@@ -0,0 +1,298 @@
+package com.example.studybuddy.view.chat
+
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Send
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.domain.model.Message
+import com.example.studybuddy.ui.theme.buttonColor
+import com.example.studybuddy.ui.theme.grey
+import com.example.studybuddy.view.destinations.ChatScreenRouteDestination
+import com.example.studybuddy.view.destinations.MainScreenRouteDestination
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.navigation.popUpTo
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+@Destination
+@Composable
+fun ChatScreenRoute(
+ chatId: String,
+ navigator: DestinationsNavigator
+) {
+ val viewModel: ChatListViewModel = hiltViewModel()
+ ChatScreen(
+ chatId = chatId,
+ onSendMessage = { messageText ->
+ viewModel.onSendMessage(chatId, messageText)
+ },
+ onBackClick = { navigator.navigate(MainScreenRouteDestination(1)) {
+ popUpTo(ChatScreenRouteDestination) { inclusive = true }
+ viewModel.depopulateMessage()
+ }}
+ )
+}
+
+@Composable
+fun ChatScreen(
+ chatId: String,
+ onSendMessage: (String) -> Unit,
+ onBackClick: () -> Unit = {}
+) {
+ val vm: ChatListViewModel = hiltViewModel()
+ val myUser = vm.userData.value
+ val currentChat = vm.chats.value.firstOrNull { it.chatId == chatId }
+ val chatUser = if (myUser?.userId == currentChat?.user1?.userId) currentChat?.user2 else currentChat?.user1
+ val messages = vm.chatMessage.value
+
+ // Group messages by date
+ val groupedMessages = messages.groupBy { getDateFromDateTime(it.time) }
+
+ LaunchedEffect(key1 = chatId) {
+ vm.getMessages(chatId)
+ }
+
+ BackHandler {
+ vm.depopulateMessage()
+ onBackClick()
+ }
+
+ Scaffold(
+ topBar = {
+ ChatScreenTopBar(
+ chatPartnerName = chatUser?.name ?: "",
+ onBackClick = onBackClick
+ )
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(WindowInsets.systemBars)
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .imePadding() // Adjust the entire column when the keyboard appears
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ groupedMessages.toList().reversed().forEach { (date, messagesForDate) ->
+ item {
+ DateHeader(date)
+ }
+
+ items(messagesForDate) { message ->
+ MessageItem(
+ message = message,
+ isUserMe = message.userId == myUser?.userId,
+ time = convertTimeTo12HourFormat(message.time)
+ )
+ }
+ }
+ }
+ MessageInput(
+ onSendMessage = { text ->
+ onSendMessage(text)
+ },
+ modifier = Modifier
+ .navigationBarsPadding() // Handle navigation bar padding
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ChatScreenTopBar(
+ chatPartnerName: String,
+ onBackClick: () -> Unit = {}
+) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { onBackClick() }) {
+ Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Navigate Back")
+ }
+ },
+ title = { Text(text = chatPartnerName) }
+ )
+}
+
+@Composable
+fun MessageItem(message: Message, isUserMe: Boolean, time: String) {
+ Row(
+ horizontalArrangement = if (isUserMe) Arrangement.End else Arrangement.Start,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .background(
+ if (isUserMe) buttonColor else Color.LightGray,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .padding(8.dp)
+ .widthIn(max = 250.dp)
+ ) {
+ Column {
+ // Message text
+ Text(
+ text = message.message,
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 13.sp, fontWeight = FontWeight.W500),
+ color = if (isUserMe) Color.White else Color.Black,
+ modifier = Modifier.padding(bottom = 4.dp) // Space between message and time
+ )
+
+
+ // Time text aligned to the bottom end of the box
+ Text(
+ text = time,
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp),
+ color = if (isUserMe) Color.White.copy(alpha = 0.7f) else Color.Black.copy(alpha = 0.7f),
+ modifier = Modifier.align(Alignment.End)
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MessageInput(onSendMessage: (String) -> Unit, modifier: Modifier = Modifier) {
+ var text by remember { mutableStateOf("") }
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TextField(
+ value = text,
+ onValueChange = { text = it },
+ placeholder = { Text("Type a message") },
+ modifier = Modifier.weight(1f),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent
+ ),
+ shape = RoundedCornerShape(20.dp),
+ trailingIcon = {
+ IconButton(onClick = {
+ if (text.isNotEmpty()) {
+ onSendMessage(text)
+ text = ""
+ }
+ }) {
+ Icon(Icons.Default.Send, contentDescription = "Send", tint = buttonColor, modifier = Modifier.size(32.dp))
+ }
+ }
+ )
+ }
+}
+
+@Composable
+fun DateHeader(date: String) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = date,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.Black,
+ maxLines = 1,
+ modifier = Modifier
+ .background(grey, shape = RoundedCornerShape(16.dp))
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ .wrapContentSize()
+ )
+ }
+}
+
+
+// Function to extract just the date (e.g., "Aug 15, 2024") from the dateTime string
+fun getDateFromDateTime(dateTime: String): String {
+ val inputFormat = SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH)
+ val outputFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
+
+ return try {
+ val date: Date = inputFormat.parse(dateTime) ?: return ""
+ outputFormat.format(date)
+ } catch (e: Exception) {
+ Log.e("DateConversion", "Error parsing date: ${e.message}")
+ ""
+ }
+}
+
+// Function to convert time from 24-hour format to 12-hour format (e.g., "10:30 AM")
+fun convertTimeTo12HourFormat(dateTime: String): String {
+ val inputFormat = SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH)
+ val outputFormat = SimpleDateFormat("hh:mm a", Locale.getDefault())
+
+ return try {
+ val date: Date = inputFormat.parse(dateTime) ?: return ""
+ outputFormat.format(date)
+ } catch (e: Exception) {
+ Log.e("DateConversion", "Error parsing time: ${e.message}")
+ ""
+ }
+}
+
+
+// TODO: if possible fix the message issue message goes up on keyboard open
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatListViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatListViewModel.kt
new file mode 100644
index 00000000..2b1d2adf
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/ChatListViewModel.kt
@@ -0,0 +1,189 @@
+package com.example.studybuddy.view.chat
+
+import android.icu.util.Calendar
+import android.util.Log
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import com.example.studybuddy.domain.model.ChatData
+import com.example.studybuddy.domain.model.ChatUser
+import com.example.studybuddy.domain.model.Message
+import com.example.studybuddy.domain.model.UserData
+import com.example.studybuddy.utils.Chat_Data
+import com.example.studybuddy.utils.MESSAGE
+import com.example.studybuddy.utils.User_Node
+import com.example.studybuddy.utils.handleException
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.firestore.Filter
+import com.google.firebase.firestore.FirebaseFirestore
+import com.google.firebase.firestore.ListenerRegistration
+import com.google.firebase.firestore.toObject
+import com.google.firebase.firestore.toObjects
+import com.google.firebase.firestore.util.Listener
+
+class ChatListViewModel : ViewModel() {
+
+ private val auth: FirebaseAuth = FirebaseAuth.getInstance()
+ private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance()
+
+ val userData = mutableStateOf(null)
+ val isLoading = mutableStateOf(false)
+ val chats = mutableStateOf>(listOf())
+ val chatMessage = mutableStateOf>(listOf())
+ val inProgressChat = mutableStateOf(false)
+ var currentChatMessageListener: ListenerRegistration? = null
+
+ init {
+ Log.d("ChatListViewModel", "Initializing ViewModel")
+ fetchUserData()
+ }
+
+ private fun fetchUserData() {
+ Log.d("ChatListViewModel", "Fetching user data")
+ val uid = auth.currentUser?.uid
+ if (uid != null) {
+ isLoading.value = true
+ firestore.collection(User_Node).document(uid).addSnapshotListener { value, error ->
+ if (error != null) {
+ Log.d("ChatListViewModel", "Error fetching user data: ${error.message}")
+ handleException("ChatListViewModel", "Something went wrong", error)
+ }
+ if (value != null) {
+ Log.d("ChatListViewModel", "User data fetched successfully")
+ userData.value = value.toObject()
+ isLoading.value = false
+ populateChats()
+ }
+ }
+ } else {
+ Log.d("ChatListViewModel", "User is not signed in")
+ handleException("ChatListViewModel", "User is not signed in", null)
+ }
+ }
+
+ fun populateChats() {
+ Log.d("ChatListViewModel", "Populating chats")
+ val userId = userData.value?.userId ?: return
+ isLoading.value = true
+ firestore.collection(Chat_Data).where(
+ Filter.or(
+ Filter.equalTo("user1.userId", userId),
+ Filter.equalTo("user2.userId", userId)
+ )
+ ).addSnapshotListener { value, error ->
+ if (error != null) {
+ Log.d("ChatListViewModel", "Error fetching chats: ${error.message}")
+ handleException("ChatListViewModel", "Something went wrong", error)
+ }
+ if (value != null) {
+ Log.d("ChatListViewModel", "Chats populated successfully")
+ chats.value = value.documents.mapNotNull {
+ it.toObject()
+ }
+ isLoading.value = false
+ } else {
+ Log.d("ChatListViewModel", "No chats found")
+ handleException("ChatListViewModel", "Something went wrong", error)
+ isLoading.value = false
+ }
+ }
+ }
+
+ fun onAddChat(number: String) {
+ Log.d("ChatListViewModel", "Adding chat for number: $number")
+ val userId = userData.value?.userId ?: return
+ firestore.collection(Chat_Data).where(
+ Filter.or(
+ Filter.and(
+ Filter.equalTo("user1.phone", number),
+ Filter.equalTo("user2.phone", userData.value?.number)
+ ),
+ Filter.and(
+ Filter.equalTo("user1.phone", userData.value?.number),
+ Filter.equalTo("user2.phone", number)
+ )
+ )
+ ).get().addOnSuccessListener { querySnapshot ->
+ if (querySnapshot.isEmpty) {
+ Log.d("ChatListViewModel", "No existing chat found, creating new chat")
+ firestore.collection(User_Node).whereEqualTo("number", number).get()
+ .addOnSuccessListener { userSnapshot ->
+ if (userSnapshot.isEmpty) {
+ Log.d("ChatListViewModel", "Number not found")
+ handleException("ChatListViewModel", "Number not found", null)
+ } else {
+ val chatPartner = userSnapshot.toObjects()[0]
+ val id = firestore.collection(Chat_Data).document().id
+ val chat = ChatData(
+ chatId = id,
+ ChatUser(
+ userId,
+ userData.value?.name,
+ userData.value?.number
+ ),
+ ChatUser(
+ chatPartner.userId,
+ chatPartner.name,
+ chatPartner.number
+ )
+ )
+ firestore.collection(Chat_Data).document(id).set(chat)
+ Log.d("ChatListViewModel", "Chat created with partner ${chatPartner.number}")
+ }
+ }
+ .addOnFailureListener {
+ Log.d("ChatListViewModel", "Error fetching user data: ${it.message}")
+ }
+ } else {
+ Log.d("ChatListViewModel", "Chat already exists")
+ handleException("ChatListViewModel", "Chat already exists", null)
+ }
+ }
+ .addOnFailureListener {
+ Log.d("ChatListViewModel", "Error checking existing chats: ${it.message}")
+ }
+ }
+
+ fun getMessages(chatId: String) {
+ Log.d("ChatListViewModel", "Getting messages for chatId: $chatId")
+ inProgressChat.value = true
+ currentChatMessageListener = firestore.collection(Chat_Data).document(chatId).collection(
+ MESSAGE).addSnapshotListener { value, error ->
+ if (error != null) {
+ Log.d("ChatListViewModel", "Error fetching messages: ${error.message}")
+ handleException("ChatListViewModel", "Something went wrong", error)
+ }
+ if (value != null) {
+ Log.d("ChatListViewModel", "Messages fetched successfully")
+ chatMessage.value = value.documents.mapNotNull {
+ it.toObject()
+ }.sortedBy { it.time }
+ inProgressChat.value = false
+ }
+ }
+ }
+
+ fun depopulateMessage() {
+ Log.d("ChatListViewModel", "Depopulating messages")
+ chatMessage.value = listOf()
+ currentChatMessageListener?.remove()
+ currentChatMessageListener = null
+ }
+
+ fun onSendMessage(chatId: String, message: String) {
+ Log.d("ChatListViewModel", "Sending message to chatId: $chatId")
+ val time = Calendar.getInstance().time.toString()
+ val msg = Message(userId = userData.value?.userId ?: return, time = time, message = message)
+ firestore.collection(Chat_Data)
+ .document(chatId)
+ .collection(MESSAGE)
+ .document()
+ .set(msg)
+ .addOnSuccessListener {
+ Log.d("ChatListViewModel", "Message sent successfully")
+ }
+ .addOnFailureListener {
+ Log.d("ChatListViewModel", "Failed to send message: ${it.message}")
+ }
+ }
+}
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/chatListScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/chatListScreen.kt
new file mode 100644
index 00000000..797dcc5f
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/chat/chatListScreen.kt
@@ -0,0 +1,359 @@
+package com.example.studybuddy.view.chat
+
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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.draw.clip
+import androidx.compose.ui.focus.focusModifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.core.text.isDigitsOnly
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.R
+import com.example.studybuddy.domain.model.ChatData
+import com.example.studybuddy.domain.model.UserData
+import com.example.studybuddy.ui.theme.buttonColor
+import com.example.studybuddy.ui.theme.grey
+import com.example.studybuddy.view.components.CircularProgressBar
+import com.example.studybuddy.view.components.MembershipPurchaseScreen
+import com.example.studybuddy.view.destinations.ChatScreenRouteDestination
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import kotlin.random.Random
+
+
+@Destination(
+ deepLinks = [
+ DeepLink(action = Intent.ACTION_VIEW, uriPattern = "study_buddy://chat_list")
+]
+)
+
+
+@Composable
+fun ChatListScreen(navigator: DestinationsNavigator) {
+ val vm: ChatListViewModel = hiltViewModel()
+ var isSubscribed by remember { mutableStateOf(true) }
+ val inProgress = vm.isLoading
+ val showDialog = remember { mutableStateOf(false) }
+ val context: Context = LocalContext.current
+ val userData = vm.userData.value
+
+ Log.d("ChatListScreen", "userData: $userData")
+ Log.d("ChatListScreen", "current time: ${System.currentTimeMillis()}")
+
+ LaunchedEffect(Unit) {
+ userData?.let {
+ if (it.PremiumMember == true && it.expiry!! > System.currentTimeMillis()) {
+ isSubscribed = true
+ Log.d("ChatListScreen", "User is subscribed: $userData")
+ } else {
+ isSubscribed = false
+ Log.d("ChatListScreen", "User is not subscribed or subscription expired: $userData")
+ }
+ }
+ }
+
+ if (inProgress.value) {
+ CircularProgressBar()
+ } else {
+ ChatScreenContent(
+ isSubscribed = true,//isSubscribed,
+ chats = vm.chats.value,
+ showDialog = showDialog.value,
+ onFabClick = { showDialog.value = true },
+ onDismiss = { showDialog.value = false },
+ onAddChat = {
+ vm.onAddChat(it)
+ showDialog.value = false
+ Toast.makeText(context, "Connection made request", Toast.LENGTH_SHORT).show()
+ },
+ context = context,
+ userData = userData!!,
+ navigator = navigator // Pass navigator to ChatScreenContent
+ )
+ }
+}
+
+
+@Composable
+fun ChatScreenContent(
+ isSubscribed: Boolean,
+ chats: List,
+ showDialog: Boolean,
+ onFabClick: () -> Unit,
+ onDismiss: () -> Unit,
+ onAddChat: (String) -> Unit,
+ context: Context,
+ userData: UserData,
+ navigator: DestinationsNavigator // Add navigator parameter
+) {
+ if (isSubscribed) {
+ Scaffold(
+ topBar = { ChatTopBar() },
+ floatingActionButton = {
+ FAB(
+ showDialog = showDialog,
+ onFabClick = onFabClick,
+ onDismiss = onDismiss,
+ onAddChat = onAddChat,
+ context = context,
+ modifier = Modifier.offset(y = (-70.dp))
+ )
+ }
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize()
+ ) {
+ if (chats.isEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(text = "No Chats Available")
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp)
+ ) {
+ items(chats){
+ chat->
+ val chatUser =
+ if (chat.user1.userId ==userData.userId )
+ {
+ chat.user2
+ }
+ else{
+ chat.user1
+ }
+
+ CommonRow(
+ name = chatUser.name,
+ navigator = navigator, // Pass navigator
+ chatId = chat.chatId, // Pass chatId
+ onItemClicked = {
+
+ }
+ )
+
+ }
+ }
+ }
+ }
+ }
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ MembershipPurchaseScreen()
+ }
+ }
+}
+
+@Composable
+fun CommonRow(
+ name: String?,
+ navigator: DestinationsNavigator,
+ chatId: String?,
+ onItemClicked: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp)
+ .clickable {
+ onItemClicked.invoke()
+ if (chatId != null) {
+ navigator.navigate(ChatScreenRouteDestination(chatId))
+
+ }
+ },
+ elevation = CardDefaults.cardElevation(4.dp),
+ colors = CardDefaults.cardColors(Color.White),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val images = listOf(
+ R.drawable.user1,
+ R.drawable.user2,
+ R.drawable.user4,
+ R.drawable.user5,
+ R.drawable.user3,
+ R.drawable.user6,
+ R.drawable.user7,
+ R.drawable.user8,
+ R.drawable.user
+ )
+
+ // Select a random image
+ val randomImage = images[Random.nextInt(images.size)]
+
+ Image(
+ modifier = Modifier
+ .padding(8.dp)
+ .size(42.dp)
+ .clip(CircleShape),
+ painter = painterResource(id = randomImage),
+ contentDescription = "User Image"
+ )
+ Spacer(modifier = Modifier.width(70.dp))
+
+ Text(
+ text = name ?: ".....",
+ modifier = Modifier.padding(8.dp),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+}
+
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ChatTopBar() {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ text = "Chats",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
+ )
+}
+
+
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FAB(
+ showDialog: Boolean,
+ onFabClick: () -> Unit,
+ onDismiss: () -> Unit,
+ onAddChat: (String) -> Unit,
+ context: Context,
+ modifier: Modifier
+) {
+ val addMember = remember { mutableStateOf("") }
+
+ if (showDialog) {
+ AlertDialog(
+ onDismissRequest = {
+ onDismiss.invoke()
+ addMember.value = ""
+ },
+ confirmButton = {
+ Button(
+ onClick = {if (addMember.value.isEmpty() or !addMember.value.isDigitsOnly())
+ {
+ Toast.makeText(context, "Invalid Number", Toast.LENGTH_SHORT).show()
+ }
+ else{
+ onAddChat(addMember.value)
+ addMember.value = ""
+ }
+
+ },
+ colors = ButtonDefaults.buttonColors(buttonColor),
+ ) {
+ Text(text = "Add Chat")
+ }
+ },
+ title = { Text(text = "Add Chat") },
+ text = {
+ OutlinedTextField(
+ value = addMember.value,
+ onValueChange = { addMember.value = it },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = grey,
+ unfocusedIndicatorColor = grey.copy(alpha = 0.3f),
+ focusedIndicatorColor = grey.copy(alpha = 0.3f)
+ ),
+ singleLine = true
+ )
+ }
+ )
+ } else {
+ FloatingActionButton(
+ onClick = { onFabClick.invoke() },
+ containerColor = buttonColor,
+ shape = CircleShape,
+ modifier = modifier
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = "Add Chat",
+ tint = Color.White
+ )
+ }
+ }
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/AddSubject.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/AddSubject.kt
new file mode 100644
index 00000000..628a9b82
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/AddSubject.kt
@@ -0,0 +1,141 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.domain.model.Subject
+
+@Composable
+fun AddSubjectDialog(
+ isOpen: Boolean,
+ title:String = "Add/Update Subject",
+ selectedColor: List,
+ subjectName:String,
+ goalHours:String,
+ onColourChange:(List) ->Unit,
+ onSubjectNameChange:(String) ->Unit,
+ onGoalHoursChange:(String) ->Unit,
+ onDismiss: () -> Unit,
+ onConfirmBtnClick: () -> Unit
+){
+ var subjectNameError by rememberSaveable { mutableStateOf(null)}
+ var goalHoursError by rememberSaveable { mutableStateOf(null)}
+
+
+ subjectNameError = when{
+ subjectName.isBlank()-> "Please enter subject name"
+ subjectName.length < 2 -> "Subject name must be at least 2 characters"
+ subjectName.length >20 -> "Subject name must be less than 20 characters"
+
+ else -> null
+ }
+
+ goalHoursError = when{
+ goalHours.isBlank()-> "Please enter goal study hours"
+ goalHours.toFloatOrNull()==null -> "Please enter a valid number"
+ goalHours.toFloat()<=0f -> "Please enter a number greater than 0"
+ goalHours.toFloat()>1000f -> "Please enter a number less than 1000"
+
+ else -> null
+ }
+
+ if (isOpen){
+ AlertDialog(
+ title = { Text(text=title) },
+ onDismissRequest = onDismiss,
+ text = {
+ Column() {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ horizontalArrangement = Arrangement.SpaceAround
+ ) {
+ Subject.subjectCardColor.forEach {
+ colors ->
+ Box(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .border(
+ width = 1.dp,
+ color = if (colors == selectedColor) Color.Black
+ else Color.Transparent,
+ shape = CircleShape
+ )
+ .background(brush = Brush.verticalGradient(colors))
+ .clickable { onColourChange(colors) }
+
+
+ )
+
+ }
+
+
+
+ }
+ OutlinedTextField(
+ value =subjectName,
+ onValueChange =onSubjectNameChange,
+ label = { Text(text = "Subject Name")},
+ singleLine = true,
+ isError = subjectNameError != null && subjectName.isNotBlank(),
+ supportingText = { Text(text = subjectNameError.orEmpty())}
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ OutlinedTextField(
+ value =goalHours ,
+ onValueChange =onGoalHoursChange,
+ label = { Text(text = "Goal Study Hours")},
+ singleLine = true,
+ isError = goalHoursError != null && goalHours.isNotBlank(),
+ supportingText = { Text(text = goalHoursError.orEmpty())},
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
+ }
+
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirmBtnClick,
+ enabled = subjectNameError == null && goalHoursError == null) {
+ Text(text = "Save")
+ }
+
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = "Close")
+ }
+
+ }
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ConfirmBackDialog.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ConfirmBackDialog.kt
new file mode 100644
index 00000000..aca9a8c4
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ConfirmBackDialog.kt
@@ -0,0 +1,53 @@
+package com.example.studybuddy.view.components
+
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+
+@Composable
+fun ConfirmBackDialog(onConfirmBack: () -> Unit) {
+ var showDialog by remember { mutableStateOf(false) }
+
+ // Intercept back button press and show dialog
+ BackHandler(enabled = true) {
+ showDialog = true
+ }
+
+ // Show confirmation dialog when needed
+ if (showDialog) {
+ AlertDialog(
+ onDismissRequest = {
+ // Dismiss the dialog when the user touches outside the dialog or presses the back button
+ showDialog = false
+ },
+ title = { Text("Confirm Exit") },
+ text = { Text("Are you sure you want to leave?") },
+ confirmButton = {
+ Button(
+ onClick = {
+ showDialog = false
+ onConfirmBack() // Handle the confirmed back action
+ }
+ ) {
+ Text("Yes")
+ }
+ },
+ dismissButton = {
+ Button(
+ onClick = {
+ showDialog = false // Simply close the dialog
+ }
+ ) {
+ Text("No")
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/CountCards.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/CountCards.kt
new file mode 100644
index 00000000..33ce8854
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/CountCards.kt
@@ -0,0 +1,40 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun CountCards(modifier: Modifier= Modifier,
+ headingText: String,
+ count: String){
+ ElevatedCard(modifier=modifier) {
+ Column(modifier= Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = headingText,
+ style = MaterialTheme.typography.labelSmall
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(
+ text = count,
+ style = MaterialTheme.typography.bodySmall.copy(fontSize = 30.sp)
+ )
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DatePicker.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DatePicker.kt
new file mode 100644
index 00000000..c4d05d4b
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DatePicker.kt
@@ -0,0 +1,57 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DatePickerDialog
+import androidx.compose.material3.DatePickerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TaskDatePicker(
+ state: DatePickerState,
+ isOpen: Boolean,
+ onDismissRequest: () -> Unit,
+ onConfirmRequest: () -> Unit,
+ confirmButtonText: String = "OK",
+ dismissButtonText: String = "Cancel"
+)
+{
+ if (isOpen) {
+
+ DatePickerDialog(modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp),
+ onDismissRequest = { onDismissRequest() },
+ confirmButton = {
+ TextButton(onClick = onConfirmRequest) {
+ Text(text = confirmButtonText)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = dismissButtonText)
+ }
+ },
+ content = {
+ androidx.compose.material3.DatePicker(
+ state = state,
+ dateValidator = {
+ timestamp->
+ val selectedDate = Instant.ofEpochMilli(timestamp)
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate()
+
+ val currentDate = LocalDate.now(ZoneId.systemDefault())
+ selectedDate >= currentDate
+ })
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DeleteDialog.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DeleteDialog.kt
new file mode 100644
index 00000000..a05ab67e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/DeleteDialog.kt
@@ -0,0 +1,38 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+
+@Composable
+fun DeleteDialog(
+ isOpen: Boolean,
+ title:String,
+ bodyText:String,
+ onDismiss: () -> Unit,
+ onConfirmBtnClick: () -> Unit
+){
+
+
+ if (isOpen){
+ AlertDialog(
+ title = { Text(text=title) },
+ onDismissRequest = onDismiss,
+ text = {
+ Text(text = bodyText)
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirmBtnClick){ Text(text = "Delete")}
+
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = "Cancel")
+ }
+
+ }
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/MembershipScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/MembershipScreen.kt
new file mode 100644
index 00000000..3db04d65
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/MembershipScreen.kt
@@ -0,0 +1,128 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.studybuddy.ui.theme.buttonColor
+
+@Composable
+fun MembershipPurchaseScreen() {
+ var selectedPlan by remember { mutableStateOf("Monthly") }
+ val buttonText = when (selectedPlan) {
+ "Monthly" -> "Pay ₹99"
+ "Quarterly" -> "Pay ₹249"
+ "Yearly" -> "Pay ₹799"
+ else -> "Purchase"
+ }
+
+ Card(
+ modifier = Modifier
+ .wrapContentSize()
+ .padding(16.dp),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Choose Your Plan",
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 32.dp)
+ )
+
+ PlanOption(
+ title = "Monthly Plan",
+ price = "₹99/month",
+ isSelected = selectedPlan == "Monthly",
+ onClick = { selectedPlan = "Monthly" }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ PlanOption(
+ title = "Quarterly Plan",
+ price = "₹83/month",
+ isSelected = selectedPlan == "Quarterly",
+ onClick = { selectedPlan = "Quarterly" }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ PlanOption(
+ title = "Annual Plan",
+ price = "₹67/month",
+ isSelected = selectedPlan == "Yearly",
+ onClick = { selectedPlan = "Yearly" }
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Button(
+ onClick = { /* Handle purchase */ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 48.dp, vertical = 20.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = buttonColor // Set the custom color here
+ )
+ ) {
+ Text(text = buttonText)
+ }
+ }
+ }
+}
+
+@Composable
+fun PlanOption(
+ title: String,
+ price: String,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ val borderColor = if (isSelected) buttonColor else Color.Transparent
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .border(
+ width = 2.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(10.dp)
+ )
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(10.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = title, fontWeight = FontWeight.Bold, fontSize = 18.sp)
+ Text(text = price, fontSize = 16.sp)
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MembershipPurchaseScreenPreview() {
+ MembershipPurchaseScreen()
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ProgressIndicator.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ProgressIndicator.kt
new file mode 100644
index 00000000..4bcac2c2
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/ProgressIndicator.kt
@@ -0,0 +1,43 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Card
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Circle(
+)
+{
+ Card(
+ modifier = Modifier
+ .size(40.dp)
+ .background(color = Color.Gray, shape = CircleShape)
+ ) {
+
+ }
+}
+
+@Composable
+fun ShowProgress()
+{
+ Row(modifier = Modifier.fillMaxSize()) {
+Circle()
+ }
+}
+
+
+@Preview
+@Composable
+fun ShowProgressPreview()
+{
+ ShowProgress()
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/StudySession.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/StudySession.kt
new file mode 100644
index 00000000..2b683c6c
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/StudySession.kt
@@ -0,0 +1,140 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.R
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.utils.changeMillisToDateString
+
+fun LazyListScope.StudySessionList(
+ sectionTitle: String,
+ Sessions: List,
+ emptyListText: String,
+ onDeleteItemClick: (Session) -> Unit
+) {
+ item {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = sectionTitle,
+ modifier = Modifier.padding(12.dp),
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ Text(text = "See All",
+ modifier = Modifier.padding(12.dp),
+ style = MaterialTheme.typography.bodySmall)
+
+ }
+ }
+ }
+ if (Sessions.isEmpty()) {
+ item {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ modifier = Modifier
+ .size(120.dp),
+ painter = painterResource(id = R.drawable.lamp),
+ contentDescription = emptyListText
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = emptyListText,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.Gray,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+ items(Sessions) { session ->
+ SessionCard(
+ session = session,
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
+ onDeleteItemClick = { onDeleteItemClick(session) }
+ )
+ }
+}
+
+@Composable
+private fun SessionCard(
+ modifier: Modifier = Modifier,
+ session: Session,
+ onDeleteItemClick: () -> Unit = {}
+) {
+ val formattedDuration = formatDuration(session.duration)
+
+ Card(modifier = modifier) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = session.relatedToSubject,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ text = session.date.changeMillisToDateString(),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = formattedDuration,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ IconButton(onClick = { onDeleteItemClick() }) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete Session"
+ )
+ }
+ }
+ }
+}
+
+// Function to format duration in hours and minutes
+fun formatDuration(durationSeconds: Long): String {
+ if (durationSeconds < 0) return "Invalid duration"
+
+ val hours = durationSeconds / 3600
+ val minutes = (durationSeconds % 3600) / 60
+ val seconds = durationSeconds % 60
+
+ return String.format("%02dh %02dm %02ds", hours, minutes, seconds)
+}
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectCard.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectCard.kt
new file mode 100644
index 00000000..f9f0baf0
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectCard.kt
@@ -0,0 +1,48 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.R
+
+@Composable
+fun SubjectCard(
+ subjectName: String,
+ gradientColors:List,
+ onClick: () -> Unit
+){
+ Box(modifier = Modifier
+ .size(150.dp).clickable { onClick() }
+ .background(brush = Brush.verticalGradient(gradientColors),
+ shape = MaterialTheme.shapes.medium
+ )
+ )
+ {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Image(painter = painterResource(id = R.drawable.books),
+ contentDescription ="",
+ modifier = Modifier.size(80.dp))
+
+ Text(text = subjectName,
+ style = MaterialTheme.typography.headlineMedium,
+ color = Color.White,
+ maxLines = 1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectListDropDown.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectListDropDown.kt
new file mode 100644
index 00000000..cda37a24
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SubjectListDropDown.kt
@@ -0,0 +1,79 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.BottomSheetDefaults
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SheetState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.domain.model.Subject
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SubjectListDropDown(
+ sheetState:SheetState,
+ isOpen:Boolean,
+ subjects:List,
+ bottomSheetTitle:String = "Related to subject",
+ onSubjectClick:(Subject)->Unit,
+ onDismissRequest:()->Unit
+) {
+ if (isOpen){
+ ModalBottomSheet(
+ sheetState = sheetState,
+ onDismissRequest = onDismissRequest,
+ dragHandle = {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ BottomSheetDefaults.DragHandle()
+ Text(text = bottomSheetTitle)
+ Spacer(modifier = Modifier.height(10.dp))
+ Divider()
+ }
+ }
+ )
+
+ {
+ LazyColumn(
+ contentPadding = PaddingValues(16.dp)
+ )
+ {
+ items(subjects) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onSubjectClick(it) }
+ .padding(8.dp)
+ ) {
+ Text(text = it.name)
+ }
+ }
+ if (subjects.isEmpty()) {
+ item {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(10.dp),
+ text = "Ready to begin? First add a subject")
+
+ }
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SurfaceViewRendrerComposable.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SurfaceViewRendrerComposable.kt
new file mode 100644
index 00000000..b5bd9260
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/SurfaceViewRendrerComposable.kt
@@ -0,0 +1,59 @@
+package com.example.studybuddy.view.components
+
+import android.graphics.Color
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import org.webrtc.SurfaceViewRenderer
+
+@Composable
+fun SurfaceViewRendererComposable(
+ modifier: Modifier,
+ streamName: String,
+ onSurfaceReady: (SurfaceViewRenderer) -> Unit
+) {
+ AndroidView(
+ modifier = modifier,
+ factory = { context ->
+ LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ setPadding(10, 10, 10, 10) // Set padding inside the border
+ setBackgroundColor(Color.WHITE) // Border color
+
+ val titleView = TextView(context).apply {
+ text = streamName
+ textSize = 16f // Set the text size
+ setTextColor(Color.BLACK) // Set the text color
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ addView(titleView)
+
+ val frameLayout = FrameLayout(context).apply {
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ ).also {
+ it.setMargins(10, 10, 10, 10) // Margin for the border effect
+ }
+ setBackgroundResource(android.R.drawable.dialog_holo_light_frame) // Use a system resource as an example border
+
+ addView(SurfaceViewRenderer(context).also {
+ onSurfaceReady.invoke(it)
+ })
+ }
+ addView(frameLayout)
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskCheckbox.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskCheckbox.kt
new file mode 100644
index 00000000..a77c4d3a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskCheckbox.kt
@@ -0,0 +1,40 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun TaskCheckBox(
+ isCompleted: Boolean,
+ borderColor: Color,
+ onCheckBoxClicked: () -> Unit,
+)
+{
+ Box (
+ modifier = Modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .border(2.dp, borderColor, CircleShape)
+ .clickable { onCheckBoxClicked() },
+ contentAlignment = Alignment.Center
+ ){
+ AnimatedVisibility(visible = isCompleted) {
+ Icon(modifier = Modifier.size(20.dp),
+ imageVector = Icons.Rounded.Check,
+ contentDescription =null )
+ }
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskList.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskList.kt
new file mode 100644
index 00000000..9d105dfe
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/TaskList.kt
@@ -0,0 +1,137 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.R
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.utils.Priority
+import com.example.studybuddy.utils.changeMillisToDateString
+
+fun LazyListScope.TaskList(
+ sectionTitle:String,
+ Tasks:List,
+ emptyListText:String,
+ onCheckBoxClick:(Task)->Unit,
+ onTaskCardClick:(Int?)->Unit
+){
+ item {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(text = sectionTitle,
+ modifier = Modifier.padding(12.dp),
+ style = MaterialTheme.typography.bodySmall)
+
+ Text(text = "See All",
+ modifier = Modifier.padding(12.dp),
+ style = MaterialTheme.typography.bodySmall)
+
+ }
+
+
+ }
+ }
+ if (Tasks.isEmpty()){
+ item {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ modifier = Modifier
+ .size(120.dp) ,
+ painter = painterResource(id = R.drawable.todo),
+ contentDescription =emptyListText )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = emptyListText,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.Gray,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+ items(Tasks){task->
+ TaskCard(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
+ task =task ,
+ onCheckBoxClicked = { onCheckBoxClick(task) },
+ onClick = {onTaskCardClick(task.TaskId)}
+ )
+
+ }
+
+}
+
+
+@Composable
+private fun TaskCard(
+ modifier: Modifier = Modifier,
+ task: Task,
+ onCheckBoxClicked: () -> Unit,
+ onClick: () -> Unit
+)
+{
+ ElevatedCard(modifier = modifier.clickable { onClick() }
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TaskCheckBox(
+ isCompleted = task.isCompleted,
+ borderColor = Priority.fromInt(task.priority).color,
+ onCheckBoxClicked = onCheckBoxClicked,
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Text(text = task.title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleMedium,
+ textDecoration = if (task.isCompleted) {
+ TextDecoration.LineThrough
+ }
+ else TextDecoration.None)
+
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(text = task.dueDate.changeMillisToDateString(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.Gray)
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/loading.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/loading.kt
new file mode 100644
index 00000000..7324bbd5
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/loading.kt
@@ -0,0 +1,46 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CircularProgressBar() {
+ val infiniteTransition = rememberInfiniteTransition()
+ val rotation by infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = 360f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color = Color.White)
+ ) {
+ CircularProgressIndicator(
+ color = MaterialTheme.colorScheme.primary,
+ strokeWidth = 4.dp,
+ modifier = Modifier.rotate(rotation) // Apply rotation to the CircularProgressIndicator
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/toggleButton.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/toggleButton.kt
new file mode 100644
index 00000000..30aa9c8a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/components/toggleButton.kt
@@ -0,0 +1,78 @@
+package com.example.studybuddy.view.components
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.studybuddy.ui.theme.inversePrimaryDark
+import com.example.studybuddy.ui.theme.onPrimaryContainerLight
+import com.example.studybuddy.ui.theme.primaryLight
+
+@Composable
+fun CustomToggleButton(
+ checked: Boolean,
+ onCheckedClick: (Boolean) -> Unit
+) {
+ val toggleAlignment by animateDpAsState(
+ targetValue = if (checked) 32.dp else 0.dp,
+ animationSpec = tween(durationMillis = 200)
+ )
+ // Accessing theme colors
+ val colorScheme = MaterialTheme.colorScheme
+ val toggleBackgroundColor = if (checked) colorScheme.primary else colorScheme.onSurface.copy(alpha = 0.5f)
+ val cardBackgroundColor = colorScheme.background
+ Card(
+ modifier = Modifier
+ .width(60.dp)
+ .clickable { onCheckedClick(!checked) },
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = cardBackgroundColor)
+ ) {
+ Box(
+ modifier = Modifier
+ .background(toggleBackgroundColor)
+ .fillMaxWidth()
+ .padding(4.dp),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ CheckCard(
+ Modifier
+ .padding(start = toggleAlignment)
+ .size(24.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun CheckCard(
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.size(24.dp),
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = CircleShape
+ ) {
+ Box(modifier = Modifier.background(color = Color.White))
+ }
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardEvent.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardEvent.kt
new file mode 100644
index 00000000..db2ea364
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardEvent.kt
@@ -0,0 +1,22 @@
+package com.example.studybuddy.view.dashboard
+
+import androidx.compose.ui.graphics.Color
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Task
+
+sealed class DashboardEvent {
+
+ data object SaveSubject:DashboardEvent()
+
+ data object DeleteSession:DashboardEvent()
+
+ data class OnDeleteSessionButtonClicked(val session: Session):DashboardEvent()
+
+ data class OnTaskIsCompleteChange(val task: Task):DashboardEvent()
+
+ data class OnSubjectCardColorChange(val colors:List):DashboardEvent()
+
+ data class OnSubjectNameChange(val name:String):DashboardEvent()
+
+ data class OnGoalStudyHoursChange(val hours:String):DashboardEvent()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardScreen.kt
new file mode 100644
index 00000000..2eb20b9a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardScreen.kt
@@ -0,0 +1,345 @@
+package com.example.studybuddy.view.dashboard
+
+import android.content.Intent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material3.Button
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.studybuddy.R
+import com.example.studybuddy.domain.model.BottomNavigationItem
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.view.components.AddSubjectDialog
+import com.example.studybuddy.view.components.CountCards
+import com.example.studybuddy.view.components.DeleteDialog
+import com.example.studybuddy.view.components.StudySessionList
+import com.example.studybuddy.view.components.SubjectCard
+import com.example.studybuddy.view.components.TaskList
+import com.example.studybuddy.view.destinations.SessionScreenRouteDestination
+import com.example.studybuddy.view.destinations.SubjectScreenRouteDestination
+import com.example.studybuddy.view.destinations.TaskScreenRouteDestination
+import com.example.studybuddy.view.subject.SubjectScreenNavArgs
+import com.example.studybuddy.view.task.TaskScreenNavArgs
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.collectLatest
+
+@Destination(
+ deepLinks = [
+ DeepLink(action = Intent.ACTION_VIEW, uriPattern = "study_buddy://dashboard")
+ ]
+)
+@Composable
+fun DashBoardScreenRoute(
+ navigator: DestinationsNavigator
+){
+ val viewModel : DashboardViewModel = hiltViewModel()
+ val state by viewModel.state.collectAsStateWithLifecycle()
+ val task by viewModel.tasks.collectAsStateWithLifecycle()
+ val session by viewModel.session.collectAsStateWithLifecycle()
+
+ DashboardScreen(
+ state = state,
+ task = task,
+ session = session,
+ onEvent = viewModel::onEvent,
+ snackbarEvent = viewModel.snackbarEventFlow,
+ onTaskCardClick ={taskId ->
+ taskId?.let{
+ val navArgs = TaskScreenNavArgs(taskId, subjectId = null)
+ navigator.navigate(TaskScreenRouteDestination(navArgs = navArgs))
+ }
+ } ,
+ onSubjectCardClick ={
+ subjectId ->
+ subjectId?.let {
+ val navArgs = SubjectScreenNavArgs(subjectId)
+ navigator.navigate(SubjectScreenRouteDestination(navArgs))
+ }
+ } ,
+ onStartSessionButtonClick ={
+ navigator.navigate(SessionScreenRouteDestination())
+ } )
+}
+
+
+@Composable
+private fun DashboardScreen(
+ state: dashboardVariable,
+ task:List,
+ session:List,
+ onEvent: (DashboardEvent) -> Unit,
+ snackbarEvent: SharedFlow,
+ onTaskCardClick: (Int?) -> Unit,
+ onSubjectCardClick: (Int?) -> Unit,
+ onStartSessionButtonClick: () -> Unit,
+){
+
+
+ var isAddSubjectDialogOpen by rememberSaveable { mutableStateOf(false) }
+ val snackbarHostState = remember{ SnackbarHostState() }
+ LaunchedEffect(key1 = true) {
+ snackbarEvent.collectLatest { event ->
+ when (event) {
+ is SnackbarEvent.ShowSnackbar -> {
+ snackbarHostState.showSnackbar(
+ message = event.message,
+ duration = event.duration
+ )
+ }
+
+ SnackbarEvent.NavigateUp -> {}
+ }
+ }
+ }
+
+ AddSubjectDialog(
+ subjectName = state.subjectName,
+ goalHours = state.goalStudyHours,
+ onSubjectNameChange ={onEvent.invoke(DashboardEvent.OnSubjectNameChange(it))},
+ onGoalHoursChange ={onEvent.invoke(DashboardEvent.OnGoalStudyHoursChange(it))} ,
+ selectedColor = state.subjectCardColors,
+ onColourChange ={onEvent.invoke(DashboardEvent.OnSubjectCardColorChange(it))} ,
+ isOpen = isAddSubjectDialogOpen ,
+ onDismiss = {isAddSubjectDialogOpen = false },
+ onConfirmBtnClick = {onEvent.invoke(DashboardEvent.SaveSubject)
+ isAddSubjectDialogOpen = false})
+
+ var isDeleteDialogOpen by rememberSaveable { mutableStateOf(false) }
+
+ DeleteDialog(
+ title = "Delete Session?",
+ bodyText = "Are you sure you want to delete this session? Your studied hour will be reduced"+
+ " by the duration of the session. This action is irreversible",
+ isOpen = isDeleteDialogOpen ,
+ onDismiss = {isDeleteDialogOpen = false },
+ onConfirmBtnClick = {onEvent.invoke(DashboardEvent.DeleteSession)
+ isDeleteDialogOpen = false})
+
+
+ Scaffold (
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState)},
+ topBar = { DashboardTopBar() }
+ ){paddingValues ->
+ LazyColumn (modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)){
+ item {
+ CountCardSection(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ subjectCount = state.totalSubjectCount ,
+ studyHours =state.totalStudiedHours.toString() ,
+ targetHours = state.totalGoalHours.toString())
+ }
+ item{
+ SubjectCardSection(
+ modifier = Modifier.fillMaxWidth(),
+ subjectList = state.subjects,
+ onAddIconClick = {isAddSubjectDialogOpen = true},
+ onSubjectCardClick = onSubjectCardClick
+ )
+ }
+
+ item {
+ Button(onClick = onStartSessionButtonClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 48.dp, vertical = 20.dp),
+ enabled = state.subjects.isNotEmpty()
+ ) {
+ Text(text = "Start Study Session")
+ }
+ }
+ TaskList(sectionTitle ="UPCOMING TASKS" ,
+ Tasks = task,
+ emptyListText ="You don't have any task yet.\n Click + button to add new task.",
+ onCheckBoxClick = {onEvent.invoke(DashboardEvent.OnTaskIsCompleteChange(it))},
+ onTaskCardClick = onTaskCardClick
+ )
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+
+ StudySessionList(sectionTitle ="RECENT STUDY SESSION" ,
+ Sessions = session ,
+ emptyListText ="You don't have any recent study session.\n Start a study session to begin your progress.",
+ onDeleteItemClick = {onEvent.invoke(DashboardEvent.OnDeleteSessionButtonClicked(it))
+ isDeleteDialogOpen=true}
+ )
+
+ item {
+ Spacer(modifier = Modifier.height(100.dp))
+ }
+
+ }
+
+ }
+
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DashboardTopBar(){
+ CenterAlignedTopAppBar(title = {
+ Text(text = "StudyBuddy",
+ style = MaterialTheme.typography.headlineMedium)
+ })
+}
+
+@Composable
+fun BottomAppBar(){
+ NavigationBar {
+
+ }
+}
+
+@Composable
+private fun CountCardSection(
+ modifier: Modifier,
+ subjectCount: Int,
+ studyHours: String,
+ targetHours: String
+){
+ Row {
+ CountCards(
+ modifier = modifier.weight(1f),
+ headingText = "Subject Count",
+ count = "$subjectCount"
+ )
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ CountCards(
+ modifier = modifier.weight(1f),
+ headingText = "Study Hours",
+ count = studyHours
+ )
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ CountCards(
+ modifier = modifier.weight(1f),
+ headingText = "Target Hours",
+ count = targetHours
+ )
+
+
+ }
+}
+
+@Composable
+private fun SubjectCardSection(
+ modifier: Modifier,
+ subjectList:List,
+ emptyListText:String = "You don't have any subject yet.\n Click + button to add new subject." ,
+ onAddIconClick: () -> Unit,
+ onSubjectCardClick: (Int?) -> Unit
+){
+ Column(modifier = modifier) {
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(text = "SUBJECTS",
+ modifier = Modifier.padding(12.dp),
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ IconButton(onClick = onAddIconClick ) {
+ Icon(imageVector = Icons.Default.Add,
+ contentDescription ="Add Subjects" )
+
+ }
+
+ }
+ if (subjectList.isEmpty()){
+ Image(
+ modifier = Modifier
+ .size(120.dp)
+ .align(Alignment.CenterHorizontally) ,
+ painter = painterResource(id = R.drawable.books),
+ contentDescription =emptyListText )
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = emptyListText,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.Gray,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ LazyRow (
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ contentPadding = PaddingValues(start = 12.dp, end = 12.dp)
+ ){
+ items(subjectList){subject ->
+ SubjectCard(
+ subjectName = subject.name,
+ gradientColors =subject.color.map { Color(it) },
+ onClick = {onSubjectCardClick(subject.SubjectId)})
+
+ }
+
+ }
+ }
+ }
+
+
+@Preview
+@Composable
+fun BottomAppBarPreview() {
+ BottomAppBar()
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardVariable.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardVariable.kt
new file mode 100644
index 00000000..d3414ba7
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardVariable.kt
@@ -0,0 +1,16 @@
+package com.example.studybuddy.view.dashboard
+
+import androidx.compose.ui.graphics.Color
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+
+data class dashboardVariable(
+ val totalSubjectCount: Int = 0,
+ val totalStudiedHours: Float = 0f,
+ val totalGoalHours: Float= 0f,
+ val subjects:List = emptyList(),
+ val subjectName: String = "",
+ val goalStudyHours: String = "",
+ val subjectCardColors: List = Subject.subjectCardColor.random(),
+ val session: Session? = null
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardViewModel.kt
new file mode 100644
index 00000000..0ce5aa9a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/dashboard/DashboardViewModel.kt
@@ -0,0 +1,189 @@
+package com.example.studybuddy.view.dashboard
+
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.ui.graphics.toArgb
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.domain.model.repository.SessionRepository
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import com.example.studybuddy.domain.model.repository.TaskRepository
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.utils.toHours
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+
+@HiltViewModel
+class DashboardViewModel @Inject constructor(
+ private val subjectRepository: SubjectRepository,
+ private val sessionRepository: SessionRepository,
+ private val taskRepository: TaskRepository
+):ViewModel()
+{
+ private val _state = MutableStateFlow(dashboardVariable())
+
+ val state = combine(
+ _state,
+ subjectRepository.getTotalSubjectCount(),
+ subjectRepository.getTotalGoalHours(),
+ subjectRepository.getAllSubjects(),
+ sessionRepository.getTotalSessionDuration()
+ ){
+ _state, totalSubjectCount, totalGoalHours, subjects, totalSessionDuration ->
+ _state.copy(
+ totalSubjectCount = totalSubjectCount,
+ totalGoalHours = totalGoalHours,
+ subjects = subjects,
+ totalStudiedHours = totalSessionDuration.toHours()
+ )
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = dashboardVariable()
+ )
+
+
+ val tasks: StateFlow> = taskRepository.getAllUpcomingTasks()
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = emptyList()
+ )
+
+ val session: StateFlow> = sessionRepository.getRecentFiveSessions()
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = emptyList()
+ )
+
+
+ private val _snackbarEventFlow = MutableSharedFlow()
+ val snackbarEventFlow = _snackbarEventFlow.asSharedFlow()
+
+
+ fun onEvent(event: DashboardEvent){
+ when(event){
+ DashboardEvent.DeleteSession -> deleteSession()
+ is DashboardEvent.OnDeleteSessionButtonClicked -> {
+ _state.update {
+ it.copy(session = event.session)
+ }
+ }
+ is DashboardEvent.OnGoalStudyHoursChange -> {
+
+ _state.update {
+ it.copy(goalStudyHours = event.hours)
+ }
+ }
+ is DashboardEvent.OnSubjectCardColorChange -> {
+
+ _state.update {
+ it.copy(subjectCardColors = event.colors)
+ }
+ }
+ is DashboardEvent.OnSubjectNameChange -> {
+
+ _state.update {
+ it.copy(subjectName = event.name)
+ }
+ }
+ is DashboardEvent.OnTaskIsCompleteChange -> {
+ updateTask(event.task)
+ }
+ DashboardEvent.SaveSubject -> saveSubject()
+ }
+ }
+
+ private fun updateTask(task: Task) {
+ viewModelScope.launch {
+ try {
+ taskRepository.upsertTask(
+ task = task.copy(isCompleted = !task.isCompleted)
+ )
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Task marked as completed"
+ )
+ )
+ }
+ catch (e:Exception){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Couldn't update the task. ${e.message}",
+ SnackbarDuration.Long
+ )
+ )
+ }
+ }
+
+ }
+
+ private fun saveSubject() {
+ viewModelScope.launch {
+ try {
+ subjectRepository.upsertSubject(
+ subject = Subject(
+ name = state.value.subjectName,
+ goalHour = state.value.goalStudyHours.toFloatOrNull()?: 1f,
+ color = state.value.subjectCardColors.map { it.toArgb() },
+
+ )
+ )
+ _state.update {
+ it.copy(
+ subjectName = "",
+ goalStudyHours = "",
+ subjectCardColors = Subject.subjectCardColor.random()
+ )
+ }
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Subject Added Successfully"
+ )
+ )
+ }
+ catch (e:Exception){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to add subject. ${e.message}",
+ SnackbarDuration.Long
+ )
+ )
+ }
+ }
+ }
+
+ private fun deleteSession() {
+ viewModelScope.launch {
+ try {
+ state.value.session?.let {
+ sessionRepository.deleteSession(it)
+ }
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Session deleted successfully"))
+ }
+ catch (e:Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to delete Session ${e.message}", SnackbarDuration.Long))
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/ServiceHelper.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/ServiceHelper.kt
new file mode 100644
index 00000000..d63eb565
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/ServiceHelper.kt
@@ -0,0 +1,34 @@
+package com.example.studybuddy.view.session
+
+import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import com.example.studybuddy.MainActivity
+import com.example.studybuddy.utils.ServiceConstants.CLICK_REQUEST_CODE
+
+object ServiceHelper {
+
+ fun clickPendingIntent(context: Context):PendingIntent{
+ val deepLinkIntent = Intent(
+ Intent.ACTION_VIEW,
+ "study_buddy://dashboard/session".toUri(),
+ context,
+ MainActivity::class.java
+
+ )
+ return TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(deepLinkIntent)
+ getPendingIntent(CLICK_REQUEST_CODE, PendingIntent.FLAG_IMMUTABLE)
+ }
+ }
+
+ fun triggerForegroundService(context: Context, action: String) {
+ Intent(context, StudySessionTimerService::class.java).apply {
+ this.action = action
+ context.startService(this)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionEvent.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionEvent.kt
new file mode 100644
index 00000000..0997dbce
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionEvent.kt
@@ -0,0 +1,22 @@
+package com.example.studybuddy.view.session
+
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+
+sealed class SessionEvent {
+
+ data class OnRelatedSubjectChange(val subject: Subject) : SessionEvent()
+
+ data class SaveSession(val duration: Long) : SessionEvent()
+
+ data class OnDeleteSessionButtonClicked(val session: Session) : SessionEvent()
+
+ data object DeleteSession: SessionEvent()
+
+ data object NotifyToUpdateSubject : SessionEvent()
+
+ data class UpdateSubjectIdAndRelatedSubject(
+ val subjectId: Int?,
+ val relatedToSubject: String?
+ ) : SessionEvent()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreen.kt
new file mode 100644
index 00000000..21129fd0
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreen.kt
@@ -0,0 +1,433 @@
+package com.example.studybuddy.view.session
+
+import android.content.Intent
+import android.util.Log
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.studybuddy.ui.theme.Red
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Cancel
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Start
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Stop
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.view.components.DeleteDialog
+import com.example.studybuddy.view.components.StudySessionList
+import com.example.studybuddy.view.components.SubjectListDropDown
+import com.example.studybuddy.view.destinations.MainScreenRouteDestination
+import com.example.studybuddy.view.destinations.SessionScreenRouteDestination
+import com.example.studybuddy.view.destinations.SubjectScreenRouteDestination
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.navigation.popUpTo
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import kotlin.time.DurationUnit
+
+@Destination(
+ deepLinks = [
+ DeepLink(
+ action = Intent.ACTION_VIEW,
+ uriPattern = "study_buddy://dashboard/session"
+ )
+ ]
+)
+@Composable
+fun SessionScreenRoute(
+ navigator: DestinationsNavigator,
+ timerService: StudySessionTimerService
+)
+{
+ val viewModel:SessionScreenViewModel = hiltViewModel()
+ val state by viewModel.state.collectAsStateWithLifecycle()
+
+ SessionScreen(
+ state =state ,
+ onEvent = viewModel::onEvent ,
+ onBackButtonClick = {
+ navigator.navigate(MainScreenRouteDestination(0)) {
+ popUpTo(SessionScreenRouteDestination) { inclusive = true }
+ }
+ },
+ snackbarEvent = viewModel.snackbarEventFlow,
+ timerService = timerService
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+
+@Composable
+private fun SessionScreen(
+ state: SessionState,
+ snackbarEvent: SharedFlow,
+ onEvent: (SessionEvent) -> Unit,
+ onBackButtonClick: () -> Unit,
+ timerService: StudySessionTimerService
+)
+{
+ val hours by timerService.hours
+ val minutes by timerService.minute
+ val seconds by timerService.second
+ val currentTimerState by timerService.currentTimerState
+
+
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ var isSubjectDropDownOpen by rememberSaveable { mutableStateOf(false) }
+ val sheetState = rememberModalBottomSheetState()
+ var isDeleteSessionDialogOpen by rememberSaveable { mutableStateOf(false) }
+ val snackbarHostState = remember{ SnackbarHostState() }
+
+
+ LaunchedEffect(key1 = true) {
+ snackbarEvent.collectLatest { event ->
+ when (event) {
+ is SnackbarEvent.ShowSnackbar -> {
+ snackbarHostState.showSnackbar(
+ message = event.message,
+ duration = event.duration
+ )
+ }
+
+ SnackbarEvent.NavigateUp -> onBackButtonClick()
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = state.subjects) {
+ val subjectId = timerService.subjectId.value
+ onEvent(
+ SessionEvent.UpdateSubjectIdAndRelatedSubject(
+ subjectId = subjectId,
+ relatedToSubject = state.subjects.find { it.SubjectId == subjectId }?.name
+
+ )
+ )
+
+ }
+
+
+
+
+
+
+ DeleteDialog(
+ title = "Delete Session?",
+ bodyText = "Are you sure you want to delete this session? Your studied hour will be reduced"+
+ " by the duration of the session. This action is irreversible",
+ isOpen = isDeleteSessionDialogOpen ,
+ onDismiss = {isDeleteSessionDialogOpen = false },
+ onConfirmBtnClick = {
+ onEvent(SessionEvent.DeleteSession)
+ isDeleteSessionDialogOpen = false})
+
+
+ SubjectListDropDown(
+ sheetState =sheetState ,
+ isOpen = isSubjectDropDownOpen,
+ subjects = state.subjects ,
+ onDismissRequest = {isSubjectDropDownOpen=false},
+ onSubjectClick ={subject ->
+ scope.launch { sheetState.hide() }.invokeOnCompletion {
+ if (!sheetState.isVisible) {
+ isSubjectDropDownOpen = false
+ }
+
+ }
+ onEvent(SessionEvent.OnRelatedSubjectChange(subject))
+ }
+ )
+
+
+ Scaffold(
+ topBar = {
+ SessionScreenTopBar(
+ onBackButtonClick = onBackButtonClick,
+
+ )
+ },
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
+ ) {paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues)
+ .padding(horizontal = 12.dp)
+ ) {
+ item {
+ TimerSection(modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ hours = hours,
+ minutes = minutes,
+ seconds = seconds)
+
+ Text(text = "Related to subject")
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = state.relatedToSubject ?: "Select Subject",
+ style = MaterialTheme.typography.bodyLarge
+ )
+
+ IconButton(
+ onClick = { isSubjectDropDownOpen=true },
+ enabled = seconds == "00"
+ ) {
+ Icon(imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription ="Select Subject" )
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ item {
+ TimerButtonSection(
+ modifier = Modifier.fillMaxWidth(),
+ onStartButtonClick = {
+ if (state.subjectId!= null && state.relatedToSubject != null){
+ ServiceHelper.triggerForegroundService(
+ context = context,
+ action = if (currentTimerState == StudySessionTimerService.TimerState.STARTED){
+ Action_Service_Stop
+ }
+ else Action_Service_Start
+ )
+ timerService.subjectId.value = state.subjectId
+ }
+ else{
+ onEvent(SessionEvent.NotifyToUpdateSubject)
+ }
+
+ },
+ onCancelButtonClick = {
+ ServiceHelper.triggerForegroundService(
+ context = context,
+ action = Action_Service_Cancel
+ )
+ },
+ onFinishButtonClick = {
+ val duration = timerService.duration.toLong(DurationUnit.SECONDS)
+ onEvent(SessionEvent.SaveSession(duration))
+ if (duration>= 36)
+ {
+ ServiceHelper.triggerForegroundService(
+ context = context,
+ action = Action_Service_Cancel
+ )
+ }
+ },
+ timerState = currentTimerState,
+ seconds = seconds
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ }
+
+ StudySessionList(sectionTitle ="RECENT STUDY SESSION" ,
+ Sessions = state.sessions ,
+ emptyListText ="You don't have any recent study session.\n Start a study session to begin your progress.",
+ onDeleteItemClick = {
+ isDeleteSessionDialogOpen=true
+ onEvent(SessionEvent.OnDeleteSessionButtonClicked(it))
+ }
+ )
+
+ }
+
+ }
+
+
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SessionScreenTopBar(
+
+ onBackButtonClick: () -> Unit,
+)
+{
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = onBackButtonClick ) {
+ Icon(imageVector = Icons.Default.ArrowBack,
+ contentDescription ="Navigate Back" )
+ }
+ },
+ title = {
+ Text(text = "Session Screen")
+ }
+ )
+}
+
+@Composable
+private fun TimerSection
+ (
+ modifier: Modifier,
+ hours:String,
+ minutes:String,
+ seconds:String
+)
+{
+ Box(modifier = modifier,
+ contentAlignment = Alignment.Center)
+ {
+ Box(
+ modifier = Modifier
+ .size(250.dp)
+ .border(5.dp, MaterialTheme.colorScheme.surfaceVariant, CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Row {
+ AnimatedContent(
+ targetState = hours,
+ label = hours,
+ transitionSpec = { timerTextAnimation()}
+ ) {
+ hours ->
+ Text(text = "$hours:",
+ style = MaterialTheme.typography.titleLarge.copy(fontSize = 45.sp))
+ }
+ AnimatedContent(
+ targetState = minutes,
+ label = minutes,
+ transitionSpec = { timerTextAnimation()}
+ ) {
+ minutes ->
+ Text(text = "$minutes:",
+ style = MaterialTheme.typography.titleLarge.copy(fontSize = 45.sp))
+ }
+ AnimatedContent(
+ targetState = seconds,
+ label = seconds,
+ transitionSpec = { timerTextAnimation()}
+ ) {
+ seconds ->
+ Text(text = "$seconds",
+ style = MaterialTheme.typography.titleLarge.copy(fontSize = 45.sp))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun TimerButtonSection(
+ modifier: Modifier,
+ onStartButtonClick: () -> Unit,
+ onCancelButtonClick: () -> Unit,
+ onFinishButtonClick: () -> Unit,
+ timerState: StudySessionTimerService.TimerState,
+ seconds:String
+)
+{
+ Row(modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween) {
+ Button(
+ modifier = Modifier
+ .padding(horizontal = 5.dp, vertical = 5.dp),
+ onClick = onCancelButtonClick,
+ enabled = seconds != "00" && timerState != StudySessionTimerService.TimerState.STARTED)
+ {
+ Text(text = "Cancel")
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(horizontal = 5.dp, vertical = 5.dp),
+ onClick = onStartButtonClick,
+
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (timerState == StudySessionTimerService.TimerState.STARTED) Red
+ else MaterialTheme.colorScheme.primary,
+ contentColor = Color.White
+ )
+
+ ) {
+ Text(
+ text = when(timerState)
+ {
+
+ StudySessionTimerService.TimerState.STARTED -> "Stop"
+ StudySessionTimerService.TimerState.STOPPED -> "Resume"
+ else -> "Start"
+ })
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(horizontal = 5.dp, vertical = 5.dp),
+ onClick = { onFinishButtonClick() },
+ enabled = seconds != "00" && timerState != StudySessionTimerService.TimerState.STARTED
+ ) {
+ Text(text = "Finish")
+ }
+ }
+}
+
+private fun timerTextAnimation(duration: Int = 600): ContentTransform {
+ return slideInVertically(animationSpec = tween(duration)) { fullHeight -> fullHeight } +
+ fadeIn(animationSpec = tween(duration))togetherWith
+ slideOutVertically(animationSpec = tween(duration)) { fullHeight -> -fullHeight }+
+ fadeOut(animationSpec = tween(duration))
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreenViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreenViewModel.kt
new file mode 100644
index 00000000..e865d9df
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionScreenViewModel.kt
@@ -0,0 +1,146 @@
+package com.example.studybuddy.view.session
+
+import androidx.compose.material3.SnackbarDuration
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.repository.SessionRepository
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import com.example.studybuddy.utils.SnackbarEvent
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.time.Instant
+import javax.inject.Inject
+
+@HiltViewModel
+class SessionScreenViewModel @Inject constructor(
+ subjectRepository: SubjectRepository,
+ private val sessionRepository: SessionRepository
+
+):ViewModel() {
+
+
+ private val _state = MutableStateFlow(SessionState())
+ val state = combine(
+ _state,
+ subjectRepository.getAllSubjects(),
+ sessionRepository.getAllSessions()
+ )
+ {
+ state, subjects, sessions ->
+ state.copy(
+ subjects = subjects,
+ sessions = sessions
+ )
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = SessionState()
+ )
+ private val _snackbarEventFlow = MutableSharedFlow()
+ val snackbarEventFlow = _snackbarEventFlow.asSharedFlow()
+
+
+ fun onEvent(event: SessionEvent){
+ when(event){
+ SessionEvent.NotifyToUpdateSubject-> notifyToUpdateSubject()
+ SessionEvent.DeleteSession -> deleteSession()
+ is SessionEvent.OnDeleteSessionButtonClicked -> {
+ _state.update {
+ it.copy(session = event.session)
+ }
+ }
+ is SessionEvent.OnRelatedSubjectChange -> {
+ _state.update {
+ it.copy(
+ relatedToSubject = event.subject.name,
+ subjectId = event.subject.SubjectId
+ )
+ }
+ }
+ is SessionEvent.SaveSession -> insertSession(event.duration)
+ is SessionEvent.UpdateSubjectIdAndRelatedSubject -> {
+ _state.update {
+ it.copy(
+ relatedToSubject = event.relatedToSubject,
+ subjectId = event.subjectId
+ )
+ }
+ }
+ }
+ }
+
+ private fun notifyToUpdateSubject() {
+ viewModelScope.launch {
+ if (state.value.subjectId == null || state.value.relatedToSubject == null)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Please choose related subject to this session"))
+ }
+ }
+ }
+
+ private fun deleteSession() {
+ viewModelScope.launch {
+ try {
+ state.value.session?.let {
+ sessionRepository.deleteSession(it)
+ }
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Session deleted successfully"))
+ }
+ catch (e:Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to delete Session ${e.message}", SnackbarDuration.Long))
+ }
+ }
+ }
+
+ private fun insertSession(duration: Long) {
+ viewModelScope.launch {
+ if (duration< 36){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Single session must be more than 36 sec")
+ )
+ return@launch
+
+ }
+ try {
+ withContext(Dispatchers.IO){
+ sessionRepository.insertSession(
+ session = Session(
+ sessionSubjectId = state.value.subjectId ?: -1,
+ relatedToSubject = state.value.relatedToSubject ?: "",
+ date = Instant.now().toEpochMilli(),
+ duration = duration
+ )
+ )
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Session Saved successfully"))
+ _snackbarEventFlow.emit(SnackbarEvent.NavigateUp)
+ }
+ }
+ catch (e:Exception){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to save Session ${e.message}", SnackbarDuration.Long))
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionState.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionState.kt
new file mode 100644
index 00000000..f9583031
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/SessionState.kt
@@ -0,0 +1,12 @@
+package com.example.studybuddy.view.session
+
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+
+data class SessionState(
+ val subjects: List = emptyList(),
+ val sessions: List = emptyList(),
+ val relatedToSubject: String? = null,
+ val subjectId: Int? = null,
+ val session: Session? = null
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/StudySessionTimerService.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/StudySessionTimerService.kt
new file mode 100644
index 00000000..8d65a315
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/session/StudySessionTimerService.kt
@@ -0,0 +1,154 @@
+package com.example.studybuddy.view.session
+
+import android.annotation.SuppressLint
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import androidx.compose.runtime.mutableStateOf
+import androidx.core.app.NotificationCompat
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Cancel
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Start
+import com.example.studybuddy.utils.ServiceConstants.Action_Service_Stop
+import com.example.studybuddy.utils.ServiceConstants.NOTIFICATION_CHANNEL_ID
+import com.example.studybuddy.utils.ServiceConstants.NOTIFICATION_CHANNEL_NAME
+import com.example.studybuddy.utils.ServiceConstants.NOTIFICATION_ID
+import com.example.studybuddy.utils.SnackbarEvent.NavigateUp.TimeToString
+import dagger.hilt.android.AndroidEntryPoint
+import java.util.Timer
+import javax.inject.Inject
+import kotlin.concurrent.fixedRateTimer
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.ZERO
+import kotlin.time.Duration.Companion.seconds
+
+
+@AndroidEntryPoint
+class StudySessionTimerService : Service() {
+
+ @Inject
+ lateinit var notificationManager: NotificationManager
+
+ @Inject
+ lateinit var notificationBuilder: NotificationCompat.Builder
+
+ private val serviceBinder = StudySessionTimerBinder()
+
+ private lateinit var timer: Timer
+ var duration: Duration = Duration.ZERO
+ var second = mutableStateOf("00")
+ private set
+
+ var minute = mutableStateOf("00")
+ private set
+
+ var hours = mutableStateOf("00")
+ private set
+
+ var currentTimerState = mutableStateOf(TimerState.IDLE)
+ private set
+
+ var subjectId = mutableStateOf(null)
+
+ override fun onBind(intent: Intent?) = serviceBinder
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ intent?.action.let {
+ when (it) {
+ Action_Service_Start -> {
+ startForegroundService()
+ StartTimer{hours,minute,second ->
+ updateNotification(hours,minute,second)
+ }
+ }
+
+
+ Action_Service_Stop -> {
+ stopTimer()
+ }
+
+ Action_Service_Cancel -> {
+ stopTimer()
+ cancelTimer()
+ stopForegroundService()
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+ }
+
+
+
+ private fun startForegroundService(){
+ createNotificationChannel()
+ startForeground(NOTIFICATION_ID,notificationBuilder.build())
+ }
+
+ private fun createNotificationChannel(){
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ NOTIFICATION_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun updateNotification(hours:String,minute:String,second:String){
+ notificationManager.notify(NOTIFICATION_ID,
+ notificationBuilder.setContentText("$hours:$minute:$second")
+ .build()
+ )
+ }
+
+ private fun StartTimer(
+ onTick:(h:String,m:String,s:String) ->Unit
+ )
+ {
+ currentTimerState.value = TimerState.STARTED
+ timer = fixedRateTimer(initialDelay = 1000L, period = 1000L){
+ duration = duration.plus(1.seconds)
+ updateTimeUnits()
+ onTick(hours.value,minute.value,second.value)
+ }
+ }
+
+ private fun updateTimeUnits(){
+ duration.toComponents { hours, minutes, seconds,_->
+ this@StudySessionTimerService.hours.value =hours.toInt().TimeToString()
+ this@StudySessionTimerService.minute.value = minutes.TimeToString()
+ this@StudySessionTimerService.second.value = seconds.TimeToString()
+ }
+
+
+ }
+
+ inner class StudySessionTimerBinder : Binder(){
+ fun getService(): StudySessionTimerService = this@StudySessionTimerService
+ }
+
+ private fun stopTimer(){
+ if (this::timer.isInitialized){
+ timer.cancel()
+ }
+ currentTimerState.value = TimerState.STOPPED
+ }
+
+ private fun cancelTimer(){
+ duration = ZERO
+ updateTimeUnits()
+ currentTimerState.value = TimerState.IDLE
+ }
+ private fun stopForegroundService(){
+ notificationManager.cancel(NOTIFICATION_ID)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+
+ }
+
+ enum class TimerState{
+ IDLE,
+ STARTED,
+ STOPPED
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/splash/SplashScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/splash/SplashScreen.kt
new file mode 100644
index 00000000..5b4461f9
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/splash/SplashScreen.kt
@@ -0,0 +1,82 @@
+package com.example.studybuddy.view.splash
+
+import android.content.Intent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.studybuddy.R
+import com.example.studybuddy.view.destinations.LoginScreenRouteDestination
+import com.example.studybuddy.view.destinations.MainScreenRouteDestination
+import com.example.studybuddy.view.destinations.SplashScreenDestination
+import com.google.firebase.auth.FirebaseAuth
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.navigation.popUpTo
+import kotlinx.coroutines.delay
+
+@Destination(
+ start = true,
+ deepLinks = [
+ DeepLink(
+ action = Intent.ACTION_VIEW,
+ uriPattern = "study_buddy://splash")
+ ]
+)
+
+@Composable
+fun SplashScreen(
+ navigator: DestinationsNavigator
+) {
+ LaunchedEffect(Unit) {
+ delay(2000) // Optional delay to show splash screen
+
+ val isLoggedIn = FirebaseAuth.getInstance().currentUser != null
+
+ if (isLoggedIn) {
+ navigator.navigate(MainScreenRouteDestination(0)) {
+ popUpTo(SplashScreenDestination) { inclusive = true }
+ }
+ } else {
+ navigator.navigate(LoginScreenRouteDestination) {
+ popUpTo(SplashScreenDestination) { inclusive = true }
+ }
+ }
+ }
+
+ Surface(modifier = Modifier.fillMaxSize()) {
+ SplashScreenDesign()
+ }
+}
+
+
+
+@Composable
+fun SplashScreenDesign() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+
+
+ Image(
+ painter = painterResource(id = R.drawable.splash_screen), // Replace with your logo resource
+ contentDescription = "App Logo",
+ modifier = Modifier.fillMaxSize(), // Adjust size as needed
+ contentScale = ContentScale.Crop
+ )
+ }
+}
+
+// TODO: implement some sort of animation to the splash screen
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectEvent.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectEvent.kt
new file mode 100644
index 00000000..e6e279d2
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectEvent.kt
@@ -0,0 +1,26 @@
+package com.example.studybuddy.view.subject
+
+import androidx.compose.ui.graphics.Color
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.view.dashboard.DashboardEvent
+
+sealed class SubjectEvent {
+ data object UpdateSubject : SubjectEvent()
+
+ data object DeleteSubject : SubjectEvent()
+
+ data object DeleteSession : SubjectEvent()
+
+ data object UpdateProgress: SubjectEvent()
+
+ data class OnTaskIsCompleteChange(val task: Task): SubjectEvent()
+
+ data class OnSubjectCardColorChange(val colors:List): SubjectEvent()
+
+ data class OnSubjectNameChange(val name:String): SubjectEvent()
+
+ data class OnGoalStudyHoursChange(val hours:String): SubjectEvent()
+
+ data class OnDeleteSessionButtonClicked(val session: Session): SubjectEvent()
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectScreen.kt
new file mode 100644
index 00000000..7c91a6a6
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectScreen.kt
@@ -0,0 +1,350 @@
+package com.example.studybuddy.view.subject
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.view.components.AddSubjectDialog
+import com.example.studybuddy.view.components.CountCards
+import com.example.studybuddy.view.components.DeleteDialog
+import com.example.studybuddy.view.components.StudySessionList
+import com.example.studybuddy.view.components.TaskList
+import com.example.studybuddy.view.destinations.MainScreenRouteDestination
+import com.example.studybuddy.view.destinations.SubjectScreenRouteDestination
+import com.example.studybuddy.view.destinations.TaskScreenRouteDestination
+import com.example.studybuddy.view.task.TaskScreenNavArgs
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.navigation.popUpTo
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.collectLatest
+
+data class SubjectScreenNavArgs(
+ val subjectId:Int
+)
+
+@Destination(navArgsDelegate = SubjectScreenNavArgs::class)
+@Composable
+fun SubjectScreenRoute(
+ navigator: DestinationsNavigator,
+){
+
+ val viewModel:SubjectViewModel = hiltViewModel()
+ val state by viewModel.state.collectAsStateWithLifecycle()
+
+ SubjectScreen(
+ state =state ,
+ onEvent =viewModel::onEvent,
+ snackbarEvent = viewModel.snackbarEventFlow,
+ onBackButtonClicked = {
+ navigator.navigate(MainScreenRouteDestination(0)) {
+ popUpTo(SubjectScreenRouteDestination) { inclusive = true }
+ }
+ },
+ onAddTaskButtonClick = {
+ val navArgs = TaskScreenNavArgs(taskId = null, subjectId = state.currentSubjectId)
+ navigator.navigate(TaskScreenRouteDestination(navArgs = navArgs))
+ },
+ onTaskCardClick = { taskId ->
+ taskId?.let{
+ val navArgs = TaskScreenNavArgs(taskId, subjectId = null)
+ navigator.navigate(TaskScreenRouteDestination(navArgs = navArgs))
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SubjectScreen(
+ state: subjectState,
+ onEvent: (SubjectEvent) -> Unit,
+ snackbarEvent: SharedFlow,
+ onBackButtonClicked: () -> Unit,
+ onAddTaskButtonClick:() -> Unit,
+ onTaskCardClick: (Int?) -> Unit
+){
+
+
+ var isEditSubjectDialogOpen by rememberSaveable { mutableStateOf(false) }
+
+
+ var isDeleteSubjectDialogOpen by rememberSaveable { mutableStateOf(false) }
+ var isDeleteSessionDialogOpen by rememberSaveable { mutableStateOf(false) }
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(key1 = true) {
+ snackbarEvent.collectLatest { event ->
+ when (event) {
+ is SnackbarEvent.ShowSnackbar -> {
+ snackbarHostState.showSnackbar(
+ message = event.message,
+ duration = event.duration
+ )
+ }
+
+ SnackbarEvent.NavigateUp -> onBackButtonClicked()
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = state.studiedHours, key2 = state.goalStudyHours) {
+ onEvent(SubjectEvent.UpdateProgress)
+ }
+
+
+ AddSubjectDialog(
+ subjectName = state.subjectName,
+ goalHours = state.goalStudyHours,
+ onSubjectNameChange ={onEvent(SubjectEvent.OnSubjectNameChange(it))} ,
+ onGoalHoursChange ={onEvent(SubjectEvent.OnGoalStudyHoursChange(it))} ,
+ selectedColor = state.subjectCardColor,
+ onColourChange ={onEvent(SubjectEvent.OnSubjectCardColorChange(it))} ,
+ isOpen = isEditSubjectDialogOpen ,
+ onDismiss = {isEditSubjectDialogOpen = false },
+ onConfirmBtnClick = {onEvent(SubjectEvent.UpdateSubject)
+ isEditSubjectDialogOpen = false})
+
+
+
+ DeleteDialog(
+ title = "Delete Session?",
+ bodyText = "Are you sure you want to delete this session? Your studied hour will be reduced"+
+ " by the duration of the session. This action is irreversible",
+ isOpen = isDeleteSessionDialogOpen ,
+ onDismiss = {isDeleteSessionDialogOpen = false },
+ onConfirmBtnClick = {onEvent(SubjectEvent.DeleteSession)
+ isDeleteSessionDialogOpen = false})
+
+ DeleteDialog(
+ title = "Delete Subject?",
+ bodyText = "Are you sure you want to delete this subject? All related"+
+ " task and session will be permanently deleted. This action is irreversible",
+ isOpen = isDeleteSubjectDialogOpen ,
+ onDismiss = {isDeleteSubjectDialogOpen = false },
+ onConfirmBtnClick = {onEvent(SubjectEvent.DeleteSubject)
+ isDeleteSubjectDialogOpen = false
+ })
+
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ val listState = rememberLazyListState()
+ val isFloatingBtnExpanded by remember {
+ derivedStateOf { listState.firstVisibleItemIndex==0 }
+ }
+
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {SubjectScreenTopBar(
+ title = state.subjectName,
+ onBackButtonClicked = onBackButtonClicked,
+ onDeleteButtonClicked = { isDeleteSubjectDialogOpen=true },
+ onEditButtonClicked = { isEditSubjectDialogOpen=true },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ onClick = onAddTaskButtonClick,
+ icon = { Icon(Icons.Default.Add, contentDescription = "Add Task") },
+ text = { Text("Add Task")},
+ expanded = isFloatingBtnExpanded
+ )
+ },
+ snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
+ ) {paddingValues ->
+
+ LazyColumn (
+ state = listState,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues)){
+
+ item {
+ SubjectOverviewSection(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ studiedHours = state.studiedHours.toString(),
+ goalHours = state.goalStudyHours,
+ progress =state.progress
+ )
+ }
+ TaskList(sectionTitle ="UPCOMING TASKS" ,
+ Tasks = state.upcomingTasks,
+ emptyListText ="You don't have any task yet.\n Click + button to add new task.",
+ onCheckBoxClick = {onEvent(SubjectEvent.OnTaskIsCompleteChange(it))},
+ onTaskCardClick = onTaskCardClick
+ )
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+
+ TaskList(sectionTitle ="COMPLETED TASKS" ,
+ Tasks = state.completedTasks,
+ emptyListText ="You don't have any completed task yet.\n"+
+ "Click the checkbox to complete task",
+ onCheckBoxClick = {onEvent(SubjectEvent.OnTaskIsCompleteChange(it))},
+ onTaskCardClick = onTaskCardClick
+ )
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+
+ StudySessionList(sectionTitle ="RECENT STUDY SESSION" ,
+ Sessions = state.recentSessions ,
+ emptyListText ="You don't have any recent study session.\n Start a study session to begin your progress.",
+ onDeleteItemClick = {isDeleteSessionDialogOpen=true
+ onEvent(SubjectEvent.OnDeleteSessionButtonClicked(it))}
+ )
+
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ }
+
+ }
+
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SubjectScreenTopBar(title:String,
+ onBackButtonClicked:()->Unit,
+ onDeleteButtonClicked:()->Unit,
+ onEditButtonClicked:()->Unit,
+ scrollBehavior:TopAppBarScrollBehavior) {
+ LargeTopAppBar(
+ scrollBehavior =scrollBehavior ,
+ navigationIcon = {
+ IconButton(onClick = onBackButtonClicked) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ },
+ actions = {
+ IconButton(onClick = onDeleteButtonClicked ) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete Subject"
+ )
+
+ }
+ IconButton(onClick = onEditButtonClicked) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = "Edit Subject"
+ )
+ }
+ }
+
+
+ )
+}
+
+@Composable
+private fun SubjectOverviewSection(
+ modifier: Modifier,
+ studiedHours:String,
+ goalHours:String,
+ progress:Float
+){
+ val percentProgress = remember(progress) {
+ (progress * 100).toInt().coerceIn(0,100)
+
+ }
+ Row(
+ modifier= modifier,
+ horizontalArrangement = Arrangement.SpaceAround,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CountCards(modifier = Modifier.weight(1f),
+ headingText = "Goal Study Hours",
+ count = goalHours)
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ CountCards(modifier = Modifier.weight(1f),
+ headingText = "Study Hours",
+ count = studiedHours)
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ Box(modifier = Modifier.size(75.dp),
+ contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(modifier = Modifier
+ .fillMaxSize(),
+ progress = 1f,
+ strokeWidth = 4.dp,
+ strokeCap = StrokeCap.Round,
+ color = MaterialTheme.colorScheme.surfaceVariant
+ )
+ CircularProgressIndicator(modifier = Modifier
+ .fillMaxSize(),
+ progress = progress,
+ strokeWidth = 4.dp,
+ strokeCap = StrokeCap.Round
+ )
+
+ Text(text = "$percentProgress%")
+ }
+
+ }
+}
+
+
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectViewModel.kt
new file mode 100644
index 00000000..fd5245d8
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/SubjectViewModel.kt
@@ -0,0 +1,243 @@
+package com.example.studybuddy.view.subject
+
+import android.annotation.SuppressLint
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.domain.model.repository.SessionRepository
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import com.example.studybuddy.domain.model.repository.TaskRepository
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.utils.toHours
+import com.example.studybuddy.view.dashboard.dashboardVariable
+import com.example.studybuddy.view.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@HiltViewModel
+class SubjectViewModel @Inject constructor(
+ private val subjectRepository: SubjectRepository,
+ private val sessionRepository: SessionRepository,
+ private val taskRepository: TaskRepository,
+ savedStateHandle: SavedStateHandle
+): ViewModel() {
+
+
+
+ private val navArgs: SubjectScreenNavArgs = savedStateHandle.navArgs()
+
+private val _state = MutableStateFlow(subjectState())
+ val state = combine(
+ _state,
+ taskRepository.getTaskBySubjectId(navArgs.subjectId),
+ taskRepository.getCompletedTaskBySubjectId(navArgs.subjectId),
+ sessionRepository.getRecentFiveSessions(),
+ sessionRepository.getTotalSessionDurationBySubjectId(navArgs.subjectId)
+ )
+ {
+ state, upcomingTask, completedTask, recentSession, totalSessionDuration ->
+ state.copy(
+ upcomingTasks = upcomingTask,
+ completedTasks = completedTask,
+ recentSessions = recentSession,
+ studiedHours = totalSessionDuration.toHours()
+ )
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = subjectState()
+ )
+
+ private val _snackbarEventFlow = MutableSharedFlow()
+ val snackbarEventFlow = _snackbarEventFlow.asSharedFlow()
+
+
+ init {
+ fetchSubject()
+ }
+ fun onEvent(event: SubjectEvent)
+ {
+ when(event){
+ SubjectEvent.DeleteSession -> deleteSession()
+ SubjectEvent.DeleteSubject -> deleteSubject()
+ is SubjectEvent.OnDeleteSessionButtonClicked -> {
+ _state.update {
+ it.copy(session = event.session)
+ }
+ }
+ is SubjectEvent.OnGoalStudyHoursChange -> {
+ _state.update {
+ it.copy(
+ goalStudyHours = event.hours
+ )
+ }
+ }
+ is SubjectEvent.OnSubjectCardColorChange -> {
+ _state.update {
+ it.copy(
+ subjectCardColor = event.colors
+ )
+ }
+ }
+ is SubjectEvent.OnSubjectNameChange -> {
+ _state.update {
+ it.copy(
+ subjectName = event.name
+ )
+ }
+ }
+ is SubjectEvent.OnTaskIsCompleteChange -> {updateTask(event.task)}
+ SubjectEvent.UpdateSubject -> updateSubject()
+ SubjectEvent.UpdateProgress -> {
+ val goalStudyHours = state.value.goalStudyHours.toFloatOrNull()?: 1f
+ _state.update {
+ it.copy(
+ progress = (state.value.studiedHours / goalStudyHours).coerceIn(0f,1f)
+ )
+ }
+ }
+ }
+ }
+
+
+ private fun updateSubject() {
+ viewModelScope.launch {
+ try {
+ subjectRepository.upsertSubject(
+ subject = Subject(
+ SubjectId = state.value.currentSubjectId,
+ name = state.value.subjectName,
+ goalHour = state.value.goalStudyHours.toFloatOrNull()?: 1f,
+ color = state.value.subjectCardColor.map { it.toArgb() }
+ )
+ )
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Subject Updated Succesfully"
+ )
+ )
+ }
+ catch (e: Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to update subject. ${e.message}"
+ )
+ )
+ }
+ }
+ }
+
+
+ private fun fetchSubject(){
+ viewModelScope.launch {
+ subjectRepository
+ .getSubjectById(navArgs.subjectId)?.let { subject->
+ _state.update {
+ it.copy(
+ subjectName = subject.name,
+ goalStudyHours = subject.goalHour.toString(),
+ subjectCardColor = subject.color.map { Color(it) },
+ currentSubjectId = subject.SubjectId
+ )
+ }
+ }
+ }
+ }
+
+ private fun updateTask(task: Task) {
+ viewModelScope.launch {
+ try {
+ taskRepository.upsertTask(
+ task = task.copy(isCompleted = !task.isCompleted)
+ )
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Task marked as upcoming task"
+ )
+ )
+ }
+ catch (e:Exception){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Couldn't update the task. ${e.message}",
+ SnackbarDuration.Long
+ )
+ )
+ }
+ }
+
+ }
+
+ @SuppressLint("SuspiciousIndentation")
+ private fun deleteSubject()
+ {
+ viewModelScope.launch {
+ try {
+ val currentSubjectId = state.value.currentSubjectId
+
+ if (currentSubjectId!=null)
+ {
+ withContext(Dispatchers.IO){
+ subjectRepository.deleteSubjectById(subjectId = currentSubjectId)
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Subject Deleted Successfully")
+ )
+ _snackbarEventFlow.emit(SnackbarEvent.NavigateUp)
+ }
+
+ }
+
+ else{
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Subject not found")
+ )
+ }
+
+ }
+ catch (e:Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Failed to delete subject. ${e.message}",
+ duration = SnackbarDuration.Long)
+
+ )
+ }
+
+ }
+ }
+ private fun deleteSession() {
+ viewModelScope.launch {
+ try {
+ state.value.session?.let {
+ sessionRepository.deleteSession(it)
+ }
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Session deleted successfully"))
+ }
+ catch (e:Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to delete Session ${e.message}", SnackbarDuration.Long))
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/subjectState.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/subjectState.kt
new file mode 100644
index 00000000..608ed0ed
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/subject/subjectState.kt
@@ -0,0 +1,20 @@
+package com.example.studybuddy.view.subject
+
+import androidx.compose.ui.graphics.Color
+import com.example.studybuddy.domain.model.Session
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.domain.model.Task
+
+data class subjectState(
+ val currentSubjectId: Int? = null,
+ val subjectName: String = "",
+ val goalStudyHours: String = "",
+ val studiedHours: Float = 0f,
+ val subjectCardColor:List = Subject.subjectCardColor.random(),
+ val upcomingTasks:List = emptyList(),
+ val completedTasks:List = emptyList(),
+ val recentSessions:List = emptyList(),
+ val session:Session? = null,
+ val progress: Float = 0f,
+ val isLoading : Boolean = false
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/AlarmReceiver.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/AlarmReceiver.kt
new file mode 100644
index 00000000..56d2cb09
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/AlarmReceiver.kt
@@ -0,0 +1,85 @@
+package com.example.studybuddy.view.task
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.os.Handler
+import android.widget.Toast
+import androidx.core.app.NotificationCompat
+import androidx.core.app.TaskStackBuilder
+import com.example.studybuddy.MainActivity
+import kotlin.random.Random
+
+class AlarmReceiver : BroadcastReceiver() {
+
+ companion object {
+ private var ringtone: Ringtone? = null
+ }
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == "com.example.studybuddy.STOP_ALARM") {
+ stopAlarm()
+ } else {
+ try {
+ val taskId = intent?.getIntExtra("task_id", -1)
+ val taskTitle = intent?.getStringExtra("taskTitle")
+ Toast.makeText(context, "Reminder for task: $taskTitle", Toast.LENGTH_SHORT).show()
+ showNotification(context, taskTitle, taskId)
+ ringAlarm(context)
+ } catch (e: Exception) {
+ Toast.makeText(context, "Error on receiver: $e", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun showNotification(context: Context?, taskTitle: String?, taskId: Int?) {
+ val manager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notificationId = Random.nextInt()
+ val channelId = "reminder_channel"
+ val channelName = "Reminder Channel"
+
+ val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
+ manager.createNotificationChannel(channel)
+
+ val stopAlarmIntent = Intent(context, this.javaClass).apply {
+ action = "com.example.studybuddy.STOP_ALARM" // Unique action string
+ putExtra("notification_id", notificationId)
+ }
+ val stopAlarmPendingIntent = PendingIntent.getBroadcast(context, 0, stopAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+
+ val intent = Intent(context, MainActivity::class.java).apply {
+ putExtra("task_id", taskId)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ val pendingIntent = TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(intent)
+ getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ val builder = NotificationCompat.Builder(context, channelId)
+ .setContentTitle("Reminder")
+ .setContentText("Task: $taskTitle")
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setContentIntent(pendingIntent)
+ .addAction(android.R.drawable.ic_media_pause, "Stop Alarm", stopAlarmPendingIntent)
+ .setAutoCancel(true)
+
+ manager.notify(notificationId, builder.build())
+ }
+
+ private fun ringAlarm(context: Context?) {
+ ringtone = RingtoneManager.getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
+ ringtone?.play()
+
+ Handler().postDelayed({ stopAlarm() }, 20000) // Stop after 20 seconds
+ }
+
+ private fun stopAlarm() {
+ ringtone?.stop()
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskEvent.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskEvent.kt
new file mode 100644
index 00000000..580a41a6
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskEvent.kt
@@ -0,0 +1,26 @@
+package com.example.studybuddy.view.task
+
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.utils.Priority
+
+sealed class TaskEvent {
+ data class OnTitleChanged(val title: String) : TaskEvent()
+
+ data class OnDescriptionChanged(val description: String) : TaskEvent()
+
+ data class OnDateChanged(val millis: Long?) : TaskEvent()
+
+ data class OnTimeChanged(val timeInMillis: Long?) : TaskEvent()
+
+ data class onSetReminderchanged(val setReminder: Boolean):TaskEvent()
+
+ data class OnPriorityChanged(val priority: Priority) : TaskEvent()
+
+ data class OnRelatedSubjectSelect(val subject: Subject) : TaskEvent()
+
+ data object OnIsCompletedChanged : TaskEvent()
+
+ data object SaveTask : TaskEvent()
+
+ data object DeleteTask : TaskEvent()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskScreen.kt
new file mode 100644
index 00000000..4d38d22e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskScreen.kt
@@ -0,0 +1,485 @@
+package com.example.studybuddy.view.task
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.app.TimePickerDialog
+import android.content.Context
+import android.content.Intent
+import android.icu.util.Calendar
+import android.widget.TimePicker
+import android.widget.Toast
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.studybuddy.R
+import com.example.studybuddy.utils.Priority
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.utils.changeMillisToDateString
+import com.example.studybuddy.utils.millisToTimeString
+import com.example.studybuddy.view.components.CustomToggleButton
+import com.example.studybuddy.view.components.DeleteDialog
+import com.example.studybuddy.view.components.SubjectListDropDown
+import com.example.studybuddy.view.components.TaskCheckBox
+import com.example.studybuddy.view.components.TaskDatePicker
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+
+data class TaskScreenNavArgs(
+ val taskId:Int?,
+ val subjectId:Int?
+)
+
+
+@Destination(navArgsDelegate = TaskScreenNavArgs::class)
+@Composable
+fun TaskScreenRoute(
+ navigator:DestinationsNavigator
+){
+
+ val viewModel :TaskViewModel = hiltViewModel()
+ val state by viewModel.state.collectAsStateWithLifecycle()
+
+ TaskScreen(
+ state =state ,
+ onEvent = viewModel::onEvent,
+ snackbarEvent = viewModel.snackbarEventFlow,
+ onBackButtonClick = {navigator.navigateUp()}
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TaskScreen(
+ state: TaskState,
+ snackbarEvent: SharedFlow,
+ onEvent: (TaskEvent) -> Unit,
+ onBackButtonClick: () -> Unit
+)
+{
+
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ var isSubjectDropDownOpen by rememberSaveable { mutableStateOf(false) }
+ val sheetState = rememberModalBottomSheetState()
+ var isTaskDatePickerOpen by rememberSaveable { mutableStateOf(false) }
+ val datePickerState = rememberDatePickerState(initialSelectedDateMillis = System.currentTimeMillis())
+ var isDeleteTaskDialogOpen by rememberSaveable { mutableStateOf(false) }
+ var taskTitleError by remember{mutableStateOf(null)}
+ var subjectTitle by remember { mutableStateOf("") }
+ val snackbarHostState = remember{ SnackbarHostState() }
+ var isTaskTimePickerOpen by rememberSaveable { mutableStateOf(false) }
+ val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
+ val minute = Calendar.getInstance().get(Calendar.MINUTE)
+ var isAlarmSet by remember { mutableStateOf(false) }
+
+ if (isTaskTimePickerOpen) {
+ TimePickerDialog(
+ context,
+ { _: TimePicker, selectedHour: Int, selectedMinute: Int ->
+ val calendar = Calendar.getInstance().apply {
+ set(Calendar.HOUR_OF_DAY, selectedHour)
+ set(Calendar.MINUTE, selectedMinute)
+ }
+ onEvent(TaskEvent.OnTimeChanged(calendar.timeInMillis))
+ isTaskTimePickerOpen = false
+ },
+ hour,
+ minute,
+ false
+ ).show()
+ }
+ LaunchedEffect(key1 = true) {
+ snackbarEvent.collectLatest { event ->
+ when (event) {
+ is SnackbarEvent.ShowSnackbar -> {
+ snackbarHostState.showSnackbar(
+ message = event.message,
+ duration = event.duration
+ )
+ }
+
+ SnackbarEvent.NavigateUp -> onBackButtonClick()
+ }
+ }
+ }
+
+ taskTitleError = when{
+ state.title.isBlank() -> "Please enter task title"
+ state.title.length > 30 -> "Task title cannot be more than 30 characters"
+ state.title.length<4 -> "Task title cannot be less than 4 characters"
+
+ else-> null
+ }
+
+ DeleteDialog(
+ title = "Delete Task?",
+ bodyText = "Are you sure you want to delete this task? "+
+ " This action is irreversible",
+ isOpen = isDeleteTaskDialogOpen ,
+ onDismiss = {isDeleteTaskDialogOpen = false },
+ onConfirmBtnClick = {onEvent(TaskEvent.DeleteTask)
+ isDeleteTaskDialogOpen = false}
+ )
+
+ TaskDatePicker(
+ state = datePickerState,
+ isOpen =isTaskDatePickerOpen ,
+ onDismissRequest = { isTaskDatePickerOpen=false},
+ onConfirmRequest = {
+ onEvent(TaskEvent.OnDateChanged(millis=datePickerState.selectedDateMillis))
+ isTaskDatePickerOpen = false }
+ )
+
+
+
+ SubjectListDropDown(
+ sheetState =sheetState ,
+ isOpen = isSubjectDropDownOpen,
+ subjects = state.subjects ,
+ onDismissRequest = {isSubjectDropDownOpen=false},
+ onSubjectClick ={subject ->
+ isSubjectDropDownOpen = false
+ subjectTitle = subject.name
+
+ scope.launch { sheetState.hide() }.invokeOnCompletion {
+ if (!sheetState.isVisible) {
+ isSubjectDropDownOpen = false
+ }
+ onEvent(TaskEvent.OnRelatedSubjectSelect(subject))
+ }
+ }
+ )
+
+
+ Scaffold(
+ topBar = {
+ TaskScreenTopBar(
+ isTaskExist = state.currentTaskId!=null,
+ isCompleted = state.isTaskComplete,
+ checkBoxBorderColor = state.priority.color,
+ onBackButtonClick = onBackButtonClick,
+ onCheckBoxClick = {onEvent(TaskEvent.OnIsCompletedChanged)},
+ onDeleteButtonClick = {isDeleteTaskDialogOpen = true}
+ )
+ },
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState)}
+) {paddingValues ->
+
+ Column (modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = 12.dp)
+ ){
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = state.title,
+ onValueChange = {onEvent(TaskEvent.OnTitleChanged(it))},
+ label = { Text(text = "Title")},
+ singleLine = true,
+ isError = taskTitleError != null && state.title.isNotBlank(),
+ supportingText = { Text(text = taskTitleError.orEmpty())}
+ )
+
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = state.description,
+ onValueChange ={onEvent(TaskEvent.OnDescriptionChanged(it))},
+ label = { Text(text = "Description")},
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(text = "Due Date",
+ style = MaterialTheme.typography.bodySmall
+ )
+ Row(modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Text(text = state.dueDate.changeMillisToDateString(),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ IconButton(onClick = { isTaskDatePickerOpen = true }) {
+ Icon(imageVector = Icons.Default.DateRange,
+ contentDescription ="Select Due Date" )
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(text = "Due Time",
+ style = MaterialTheme.typography.bodySmall
+ )
+ Row(modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Text(text = millisToTimeString(state.dueTime),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ IconButton(onClick = { isTaskTimePickerOpen = true }) {
+ Icon(painter = painterResource(id = R.drawable.baseline_add_alarm_24),
+ contentDescription ="Select Due Time" )
+
+ }
+
+ }
+
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(text = "Set Alarm",
+ style = MaterialTheme.typography.bodySmall
+ )
+ Row(modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Text(text = "Do you want to set alarm",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ CustomToggleButton(
+ checked = state.isReminderSet,
+ onCheckedClick = {
+ onEvent(TaskEvent.onSetReminderchanged(it))
+ isAlarmSet = state.isReminderSet
+ }
+ )
+
+ }
+
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(text = "Priority",
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Row(modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(5.dp)),) {
+ Priority.entries.forEach{
+ PriorityButton(
+ modifier = Modifier.weight(1f),
+ label = it.name,
+ backgroundColor = it.color,
+ borderColor = if (it == state.priority){
+ Color.White
+ } else Color.Transparent,
+ labelColor = if (it == state.priority){
+ Color.White
+ } else Color.White.copy(alpha = 0.7f),
+ onClick = {onEvent(TaskEvent.OnPriorityChanged(it))}
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(text = "Related to subject",
+ style = MaterialTheme.typography.bodySmall
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val firstSubject = state.subjects.firstOrNull()?.name ?: ""
+ Text(text = state.relatedToSubject?:firstSubject,
+ style = MaterialTheme.typography.bodyLarge
+ )
+
+ IconButton(onClick = { isSubjectDropDownOpen=true }) {
+ Icon(imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription =state.relatedToSubject )
+
+ }
+
+ }
+
+ Button(
+ enabled = taskTitleError==null,
+ onClick = {
+ if (state.isReminderSet){
+ setAlarm(context,state.dueDate!!,state.dueTime!!,state.title,state.currentTaskId!!)
+ }
+ else{
+ cancelAlarm(context,state.currentTaskId!!)
+ }
+ onEvent(TaskEvent.SaveTask)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ ) {
+ Text(text = "Save")
+
+ }
+
+
+
+ }
+}
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TaskScreenTopBar(
+ isTaskExist: Boolean,
+ isCompleted: Boolean,
+ checkBoxBorderColor: Color,
+ onBackButtonClick: () -> Unit,
+ onDeleteButtonClick: () -> Unit,
+ onCheckBoxClick: () -> Unit
+)
+{
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = onBackButtonClick ) {
+ Icon(imageVector = Icons.Default.ArrowBack,
+ contentDescription ="Navigate Back" )
+ }
+ },
+ title = {
+ Text(text = "Task")
+ },
+ actions = {
+ if (isTaskExist)
+ {
+ TaskCheckBox(isCompleted = isCompleted,
+ borderColor = checkBoxBorderColor,
+ onCheckBoxClicked = onCheckBoxClick
+ )
+ IconButton(onClick = onDeleteButtonClick ) {
+ Icon(imageVector = Icons.Default.Delete,
+ contentDescription ="Delete Task" )
+ }
+ }
+ }
+ )
+}
+
+
+@Composable
+private fun PriorityButton(
+ modifier: Modifier = Modifier,
+ label: String,
+ backgroundColor: Color,
+ borderColor: Color,
+ labelColor: Color,
+ onClick: () -> Unit
+)
+{
+ Box(modifier = modifier
+ .background(backgroundColor)
+ .clickable { onClick() }
+ .padding(5.dp)
+ .border(1.dp, borderColor, RoundedCornerShape(5.dp))
+ .padding(5.dp),
+ contentAlignment = Alignment.Center
+ ){
+ Text(text = label,
+ color = labelColor
+ )
+ }
+
+}
+
+
+fun setAlarm(context: Context, dueDate: Long, dueTime: Long, taskTitle: String, taskId: Int) {
+ val calendar = Calendar.getInstance().apply {
+ timeInMillis = dueDate
+ val dueTimeCalendar = Calendar.getInstance().apply {
+ timeInMillis = dueTime
+ }
+ set(Calendar.HOUR_OF_DAY, dueTimeCalendar.get(Calendar.HOUR_OF_DAY))
+ set(Calendar.MINUTE, dueTimeCalendar.get(Calendar.MINUTE))
+ set(Calendar.SECOND, 0)
+ }
+
+ val timeInMillis = calendar.timeInMillis - (5 * 60 * 1000) // Set alarm 5 minutes before the due date and time
+
+ Toast.makeText(context, "Alarm set for $timeInMillis", Toast.LENGTH_SHORT).show()
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val intent = Intent(context, AlarmReceiver::class.java).apply {
+ putExtra("taskTitle", taskTitle)
+ }
+ val pendingIntent = PendingIntent.getBroadcast(context, taskId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+
+ alarmManager.set(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+}
+
+private fun cancelAlarm(context: Context, taskId: Int) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val intent = Intent(context, AlarmReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(context, taskId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ alarmManager.cancel(pendingIntent)
+}
+
+
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskState.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskState.kt
new file mode 100644
index 00000000..de4d1a7f
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskState.kt
@@ -0,0 +1,19 @@
+package com.example.studybuddy.view.task
+
+import com.example.studybuddy.domain.model.Subject
+import com.example.studybuddy.utils.Priority
+
+data class TaskState(
+ val title:String ="",
+ val description:String ="",
+ val dueDate:Long? = null,
+ val dueTime: Long? = null,
+ val isReminderSet:Boolean = false,
+ val isTaskComplete:Boolean = false,
+ val priority: Priority = Priority.LOW,
+ val relatedToSubject:String? = null,
+ val subjects:List = emptyList(),
+ val subjectId:Int? = null,
+ val currentTaskId:Int? = null,
+
+)
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskViewModel.kt
new file mode 100644
index 00000000..d5732dd4
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/task/TaskViewModel.kt
@@ -0,0 +1,231 @@
+package com.example.studybuddy.view.task
+
+import androidx.compose.material3.SnackbarDuration
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.studybuddy.domain.model.Task
+import com.example.studybuddy.domain.model.repository.SubjectRepository
+import com.example.studybuddy.domain.model.repository.TaskRepository
+import com.example.studybuddy.utils.Priority
+import com.example.studybuddy.utils.SnackbarEvent
+import com.example.studybuddy.view.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.time.Instant
+import javax.inject.Inject
+
+
+@HiltViewModel
+class TaskViewModel @Inject constructor(
+private val taskRepository: TaskRepository,
+ private val subjectRepository: SubjectRepository,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val navArgs:TaskScreenNavArgs = savedStateHandle.navArgs()
+ private val _state = MutableStateFlow(TaskState())
+ val state = combine(
+ _state,
+ subjectRepository.getAllSubjects(),
+ ){
+ state, subjects ->
+ state.copy(subjects = subjects)
+ }.stateIn(
+ scope = viewModelScope,
+ initialValue = TaskState(),
+ started = SharingStarted.WhileSubscribed(5000)
+ )
+
+ private val _snackbarEventFlow = MutableSharedFlow()
+ val snackbarEventFlow = _snackbarEventFlow.asSharedFlow()
+
+ init {
+ fetchTaskById()
+ fetchSubject()
+ }
+
+
+ fun onEvent(event: TaskEvent){
+ when(event){
+ TaskEvent.DeleteTask -> deleteTask()
+ is TaskEvent.OnDateChanged -> {
+ _state.update {
+ it.copy(dueDate = event.millis)
+ }
+ }
+ is TaskEvent.OnDescriptionChanged -> {
+ _state.update {
+ it.copy(description = event.description)
+ }
+ }
+ TaskEvent.OnIsCompletedChanged -> {
+ _state.update {
+ it.copy(isTaskComplete = !_state.value.isTaskComplete,
+ isReminderSet = !_state.value.isReminderSet)
+ }
+ }
+ is TaskEvent.OnPriorityChanged -> {
+ _state.update {
+ it.copy(priority = event.priority)
+ }
+ }
+ is TaskEvent.OnRelatedSubjectSelect -> {
+ _state.update {
+ it.copy(relatedToSubject = event.subject.name,
+ subjectId = event.subject.SubjectId)
+ }
+ }
+ is TaskEvent.OnTitleChanged -> {
+ _state.update {
+ it.copy(title = event.title)
+ }
+ }
+ TaskEvent.SaveTask -> saveTask()
+ is TaskEvent.OnTimeChanged -> {
+ _state.update {
+ it.copy(dueTime = event.timeInMillis)
+ }
+ }
+
+ is TaskEvent.onSetReminderchanged-> {
+ _state.value = state.value.copy(isReminderSet = event.setReminder)
+ }
+
+ else -> {}
+ }
+ }
+
+ private fun saveTask() {
+ viewModelScope.launch {
+ if(state.value.subjectId ==null|| state.value.relatedToSubject==null)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Please fill all the fields before saving",SnackbarDuration.Long))
+
+
+ return@launch
+
+ }
+
+ try {
+ withContext(Dispatchers.IO){//to run it on io thread
+
+ taskRepository.upsertTask(
+ task = Task(
+ title = state.value.title,
+ description = state.value.description,
+ dueDate = state.value.dueDate ?: Instant.now().toEpochMilli(),
+ isCompleted = state.value.isTaskComplete,
+ priority = state.value.priority.value,
+ relatedToSubject = state.value.relatedToSubject!!,
+ taskSubjectId = state.value.subjectId!!,
+ TaskId = state.value.currentTaskId,
+ dueTime = state.value.dueTime?:Instant.now().toEpochMilli(),
+ setReminder = state.value.isReminderSet
+ )
+ )
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Task Saved successfully"))
+ _snackbarEventFlow.emit(SnackbarEvent.NavigateUp)
+ }
+ }
+ catch (e:Exception){
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar(
+ "Failed to save Task ${e.message}", SnackbarDuration.Long))
+ }
+ }
+ }
+
+
+ private fun deleteTask()
+ {
+ viewModelScope.launch {
+ try {
+ val currentTaskId = state.value.currentTaskId
+
+ if (currentTaskId!=null)
+ {
+ withContext(Dispatchers.IO){
+ taskRepository.deleteTaskById(currentTaskId)
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Task Deleted Successfully")
+ )
+ _snackbarEventFlow.emit(SnackbarEvent.NavigateUp)
+ }
+
+ }
+
+ else{
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Task not found")
+ )
+ }
+
+ }
+ catch (e:Exception)
+ {
+ _snackbarEventFlow.emit(
+ SnackbarEvent.ShowSnackbar("Failed to delete task. ${e.message}",
+ duration = SnackbarDuration.Long)
+
+ )
+ }
+
+ }
+ }
+
+ private fun fetchTaskById() {
+ viewModelScope.launch {
+ navArgs.taskId?.let {
+ taskRepository.getTaskByTaskId(it)?.let { task ->
+ _state.update {
+ it.copy(
+ title =task.title,
+ description = task.description,
+ dueDate = task.dueDate,
+ isTaskComplete = task.isCompleted,
+ priority = Priority.fromInt(task.priority),
+ relatedToSubject = task.relatedToSubject,
+ currentTaskId = task.TaskId,
+ subjectId = task.taskSubjectId,
+ dueTime = task.dueTime,
+ isReminderSet = task.setReminder
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun fetchSubject(){
+ viewModelScope.launch {
+ navArgs.subjectId?.let { id->
+ subjectRepository
+ .getSubjectById(navArgs.subjectId)?.let { subject->
+ _state.update {
+ it.copy(
+ subjectId = subject.SubjectId,
+ relatedToSubject = subject.name
+ )
+ }
+ }
+ }
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/CloseActivity.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/CloseActivity.kt
new file mode 100644
index 00000000..187e210e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/CloseActivity.kt
@@ -0,0 +1,11 @@
+package com.example.studybuddy.view.videocall
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+
+class CloseActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ finishAffinity()
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/ConferenceScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/ConferenceScreen.kt
new file mode 100644
index 00000000..6e8fc16a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/ConferenceScreen.kt
@@ -0,0 +1,70 @@
+package com.example.studybuddy.view.videocall
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.key
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.StudyBuddyApp
+import com.example.studybuddy.view.components.ConfirmBackDialog
+import com.example.studybuddy.view.components.SurfaceViewRendererComposable
+import com.example.studybuddy.view.videocall.viewmodel.MainViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+
+@Destination
+
+@Composable
+fun ConferenceScreenRoute(
+ roomId: String,
+ navigator: DestinationsNavigator,
+) {
+ val mainViewModel: MainViewModel = hiltViewModel()
+ ConferenceScreen(roomId, navigator, mainViewModel)
+}
+@Composable
+fun ConferenceScreen(
+ roomId: String,
+ navigator: DestinationsNavigator,
+ mainViewModel: MainViewModel
+) {
+ val streamState = mainViewModel.mediaStreamsState.collectAsState().value ?: hashMapOf()
+ val totalNumberOfStreams = 1 + streamState.count { it.key != StudyBuddyApp.username }
+
+ Column(Modifier.fillMaxSize()) {
+ Text(text = "Room name = $roomId")
+
+ val streamModifier = Modifier.fillMaxWidth().weight(1f / totalNumberOfStreams)
+
+ SurfaceViewRendererComposable(
+ modifier = streamModifier,
+ streamName = "Local",
+ onSurfaceReady = { mainViewModel.onRoomJoined(roomId!!, it) }
+ )
+
+ streamState.forEach { (streamId, mediaStream) ->
+ if (streamId != StudyBuddyApp.username) {
+ key(streamId) {
+ SurfaceViewRendererComposable(
+ modifier = streamModifier,
+ streamName = streamId,
+ onSurfaceReady = { surfaceView ->
+ mainViewModel.initRemoteSurfaceView(surfaceView)
+ mediaStream.videoTracks.firstOrNull()?.addSink(surfaceView)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ ConfirmBackDialog {
+ mainViewModel::onLeaveConferenceClicked.invoke()
+ navigator.popBackStack()
+
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/HomeScreen.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/HomeScreen.kt
new file mode 100644
index 00000000..9e439035
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/HomeScreen.kt
@@ -0,0 +1,127 @@
+package com.example.studybuddy.view.videocall
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Intent
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.example.studybuddy.view.destinations.ConferenceScreenRouteDestination
+import com.example.studybuddy.view.videocall.viewmodel.MainViewModel
+import com.ramcosta.composedestinations.annotation.DeepLink
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+
+
+@Destination(
+ deepLinks = [
+ DeepLink(action = Intent.ACTION_VIEW, uriPattern = "study_buddy://videocall")
+ ]
+)
+
+@Composable
+fun HomeScreenRoute(navigator: DestinationsNavigator){
+ val mainViewModel:MainViewModel = hiltViewModel()
+
+ HomeScreen(mainViewModel, navigator)
+
+}
+
+@SuppressLint("InlinedApi")
+@Composable
+fun HomeScreen(mainViewModel: MainViewModel,
+ navigator: DestinationsNavigator // Add navigator as a parameter) {
+){
+ val context = LocalContext.current
+ val createRoomDialog = RoomNameAlertDialog(context, object :
+ RoomNameAlertDialog.RoomNameDialogListener {
+ override fun onCreateRoomName(roomName: String) {
+ mainViewModel.onCreateRoomClicked(roomName)
+ navigator.navigate(ConferenceScreenRouteDestination(roomName))
+ }
+ })
+ val requestPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ // All permissions are granted
+ if (permissions.all { it.value }) {
+ mainViewModel.init()
+ }
+ }
+
+ LaunchedEffect(key1 = Unit){
+ requestPermissionLauncher.launch(
+ arrayOf(
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.CAMERA,
+ Manifest.permission.POST_NOTIFICATIONS,
+ )
+ )
+ }
+
+ val roomState = mainViewModel.roomsState.collectAsState()
+ Column(Modifier.fillMaxWidth().padding(top = 100.dp)) {
+ Button(
+ onClick = {
+ createRoomDialog.show()
+ },
+ Modifier
+ .padding(10.dp)
+ .height(40.dp)
+ .fillMaxWidth()
+ ) {
+ Text(text = "Create Room")
+ }
+ roomState.value?.let { roomList ->
+ LazyColumn(Modifier.weight(15f)) {
+ items(items = roomList) { item ->
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(1.dp)
+ .border(
+ width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(4.dp)
+ )
+ .padding(5.dp)
+ .clickable {
+ navigator.navigate(ConferenceScreenRouteDestination(item.roomName))
+ },
+ horizontalArrangement = Arrangement.SpaceBetween,
+
+
+ ) {
+ Text(text = "Room name: ${item.roomName}")
+ Spacer(modifier = Modifier.padding(5.dp))
+ Text(text = "Members: ${item.population}")
+
+ }
+ }
+ }
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/RoomNameAlertDialog.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/RoomNameAlertDialog.kt
new file mode 100644
index 00000000..616d978a
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/RoomNameAlertDialog.kt
@@ -0,0 +1,44 @@
+package com.example.studybuddy.view.videocall
+
+import android.app.AlertDialog
+import android.content.Context
+import android.text.TextUtils
+import android.widget.EditText
+import android.widget.Toast
+
+class RoomNameAlertDialog(
+ private val context: Context,
+ private val listener: RoomNameDialogListener
+) {
+
+ fun show() {
+ // Create an EditText for the room name input
+ val input = EditText(context).apply {
+ hint = "room name"
+ }
+
+ // Create the AlertDialog
+ val dialog = AlertDialog.Builder(context).apply {
+ setTitle("Enter Room Name")
+ setView(input)
+ setPositiveButton("Create") { a, _ ->
+ val roomName = input.text.toString()
+ if (!TextUtils.isEmpty(roomName)) {
+ listener.onCreateRoomName(roomName) // Use the listener callback here
+ a.dismiss()
+ } else {
+ Toast.makeText(context, "Room name cannot be empty.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ setNegativeButton("Cancel") { dialog, _ ->
+ dialog.cancel()
+ }
+ }.create()
+
+ dialog.show()
+ }
+
+ interface RoomNameDialogListener {
+ fun onCreateRoomName(roomName: String)
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallBroadcastReceiver.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallBroadcastReceiver.kt
new file mode 100644
index 00000000..9e320ad9
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallBroadcastReceiver.kt
@@ -0,0 +1,25 @@
+package com.example.studybuddy.view.videocall.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.example.studybuddy.view.videocall.CloseActivity
+
+
+class CallBroadcastReceiver() : BroadcastReceiver() {
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.action?.let { action ->
+ if (action == "ACTION_EXIT") {
+ context?.let { noneNullContext ->
+ CallService.stopService(noneNullContext) // Stop the service
+ noneNullContext.startActivity(
+ Intent(noneNullContext, CloseActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallService.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallService.kt
new file mode 100644
index 00000000..4db8665b
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallService.kt
@@ -0,0 +1,374 @@
+package com.example.studybuddy.view.videocall.service
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import com.example.studybuddy.MainActivity
+import com.example.studybuddy.R
+import com.example.studybuddy.StudyBuddyApp
+import com.example.studybuddy.domain.model.MessageModel
+import com.example.studybuddy.domain.model.RoomModel
+import com.example.studybuddy.view.videocall.socket.SocketClient
+import com.example.studybuddy.view.videocall.socket.SocketEventListener
+import com.example.studybuddy.view.videocall.socket.SocketEventSender
+import com.example.studybuddy.view.videocall.socket.SocketEvents.*
+import com.example.studybuddy.view.videocall.webrtc.LocalStreamListener
+import com.example.studybuddy.view.videocall.webrtc.MyPeerObserver
+import com.example.studybuddy.view.videocall.webrtc.RTCClient
+import com.example.studybuddy.view.videocall.webrtc.WebRTCFactory
+import com.example.studybuddy.view.videocall.webrtc.WebRTCSignalListener
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import org.webrtc.IceCandidate
+import org.webrtc.MediaStream
+import org.webrtc.PeerConnection
+import org.webrtc.SessionDescription
+import org.webrtc.SurfaceViewRenderer
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class CallService : Service(), SocketEventListener, WebRTCSignalListener {
+
+ @Inject
+ lateinit var socketClient: SocketClient
+
+ @Inject
+ lateinit var eventSender: SocketEventSender
+
+ @Inject
+ lateinit var gson: Gson
+
+ @Inject
+ lateinit var webRTCFactory: WebRTCFactory
+
+
+ //service section
+ private lateinit var mainNotification: NotificationCompat.Builder
+ private lateinit var notificationManager: NotificationManager
+
+ //state
+ val roomsState: MutableStateFlow?> = MutableStateFlow(null)
+ val mediaStreamsState: MutableStateFlow> = MutableStateFlow(
+ hashMapOf()
+ )
+
+ private fun getMediaStreams() = mediaStreamsState.value
+ fun addMediaStreamToState(username: String, mediaStream: MediaStream) {
+ val updatedData = HashMap(getMediaStreams()).apply {
+ put(username, mediaStream)
+ }
+ mediaStreamsState.value = updatedData
+ }
+
+ fun removeMediaStreamFromState(username: String) {
+ val updatedData = HashMap(getMediaStreams()).apply {
+ remove(username)
+ }
+ // Update the state with the new HashMap
+ mediaStreamsState.value = updatedData
+ }
+
+ //connection list
+ private val connections: MutableMap = mutableMapOf()
+
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ intent?.let {
+ when (it.action) {
+ CallServiceActions.START.name -> handleStartService()
+ CallServiceActions.STOP.name -> handleStopService()
+ else -> Unit
+ }
+ }
+ return START_STICKY
+ }
+
+ private fun handleStartService() {
+ if (!isServiceRunning) {
+ isServiceRunning = true
+ Log.d("CallService", "Service started")
+ //start service here
+ startServiceWithNotification()
+ }
+ }
+
+ private fun handleStopService() {
+ if (isServiceRunning) {
+ isServiceRunning = false
+ Log.d("CallService", "Service stopped")
+ }
+ socketClient.onStop()
+ connections.onEach {
+ runCatching {
+ it.value.onDestroy()
+ Log.d("CallService", "Connection destroyed: ${it.key}")
+ }
+ }
+ webRTCFactory.onDestroy()
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ notificationManager = getSystemService(
+ NotificationManager::class.java
+ )
+ createNotifications()
+ socketClient.setListener(this)
+ Log.d("CallService", "Service created")
+ }
+
+
+ override fun onSocketOpened() {
+ Log.d("CallService", "Socket opened")
+ eventSender.storeUser()
+ }
+
+ override fun onSocketClosed() {
+ }
+
+ override fun onNewMessage(message: MessageModel) {
+ Log.d("CallService", "New message received: ${message.type}")
+ when (message.type) {
+ RoomStatus -> handleRoomStatus(message)
+ NewSession -> handleNewSession(message)
+ StartCall -> handleStartCall(message)
+ Offer -> handleOffer(message)
+ Answer -> handleAnswer(message)
+ Ice -> handleIceCandidates(message)
+ else -> Unit
+ }
+ }
+
+ fun initializeSurface(view: SurfaceViewRenderer) {
+ webRTCFactory.init(view, object : LocalStreamListener {
+ override fun onLocalStreamReady(mediaStream: MediaStream) {
+ addMediaStreamToState(StudyBuddyApp.username, mediaStream)
+ Log.d("CallService", "Local stream ready for user: ${StudyBuddyApp.username}")
+
+ }
+ })
+ }
+
+ private fun handleNewSession(message: MessageModel) {
+ message.name?.let { target ->
+ Log.d("CallService", "Handling new session for: $target")
+ startNewConnection(target) {
+ eventSender.startCall(target)
+ }
+ }
+ }
+
+ private fun handleStartCall(message: MessageModel) {
+ //we create new connection here
+ startNewConnection(message.name!!) {
+ it.call()
+ }
+ }
+
+ private fun startNewConnection(targetName: String, done: (RTCClient) -> Unit) {
+ Log.d("CallService", "Starting new connection for: $targetName")
+ webRTCFactory.createRtcClient(object : MyPeerObserver() {
+ override fun onIceCandidate(p0: IceCandidate?) {
+ super.onIceCandidate(p0)
+ findClient(targetName)?.let {
+ Log.d("CallService", "Sending ICE candidate to peer: $targetName")
+ if (p0 != null) {
+ it.sendIceCandidateToPeer(p0, targetName)
+ }
+ }
+ }
+
+ override fun onAddStream(p0: MediaStream?) {
+ super.onAddStream(p0)
+ p0?.let {
+ addMediaStreamToState(targetName, it)
+ Log.d("CallService", "Sending ICE candidate to peer: $targetName")
+ }
+ }
+
+ override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
+ super.onConnectionChange(newState)
+ Log.d("CallService", "Connection state changed for $targetName: $newState")
+
+ if (
+ newState == PeerConnection.PeerConnectionState.CLOSED ||
+ newState == PeerConnection.PeerConnectionState.DISCONNECTED ||
+ newState == PeerConnection.PeerConnectionState.FAILED
+ ) {
+ removeMediaStreamFromState(targetName)
+ }
+ }
+ }, targetName, this).also {
+ it?.let {
+ connections[targetName] = it
+ CoroutineScope(Dispatchers.IO).launch {
+ delay(1000)
+ done(it)
+ }
+ }
+ }
+ }
+
+ private fun handleOffer(message: MessageModel) {
+ Log.d("CallService", "Handling offer from: ${message.name}")
+ findClient(message.name!!)?.let {
+ it.onRemoteSessionReceived(
+ SessionDescription(
+ SessionDescription.Type.OFFER,
+ message.data.toString()
+ )
+ )
+ it.answer()
+ }
+ }
+
+ private fun handleAnswer(message: MessageModel) {
+ Log.d("CallService", "Handling answer from: ${message.name}")
+ findClient(message.name!!).apply {
+ this?.onRemoteSessionReceived(
+ SessionDescription(
+ SessionDescription.Type.ANSWER,
+ message.data.toString()
+ )
+ )
+ }
+ }
+
+ private fun handleIceCandidates(message: MessageModel) {
+ Log.d("CallService", "Handling ICE candidates from: ${message.name}")
+ val ice = runCatching {
+ gson.fromJson(message.data.toString(), IceCandidate::class.java)
+ }
+ ice.onSuccess {
+ findClient(message.name!!).apply {
+ this?.addIceCandidateToPeer(it)
+ }
+ }
+ }
+
+ fun leaveRoom(){
+ connections.onEach {
+ it.value.onDestroy()
+ }
+ }
+
+ private fun findClient(username: String): RTCClient? {
+ return connections[username]
+ }
+
+
+ private fun handleRoomStatus(message: MessageModel) {
+ val type = object : TypeToken>() {}.type
+ val rooms: List = gson.fromJson(message.data.toString(), type)
+
+ roomsState.value = rooms
+ Log.d("CallService", "Room status updated: $rooms")
+ }
+
+
+ private fun startServiceWithNotification() {
+ startForeground(MAIN_NOTIFICATION_ID, mainNotification.build())
+ Log.d("CallService", "Service running in foreground with notification")
+ }
+
+ private val binder = LocalBinder()
+
+ inner class LocalBinder : Binder() {
+ fun getService(): CallService = this@CallService
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ return binder
+ }
+
+ private fun createNotifications() {
+ val callChannel = NotificationChannel(
+ CALL_NOTIFICATION_CHANNEL_ID,
+ CALL_NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ notificationManager.createNotificationChannel(callChannel)
+ val contentIntent = Intent(
+ this, MainActivity::class.java
+ ).apply {
+ flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ }
+ val contentPendingIntent = PendingIntent.getActivity(
+ this,
+ System.currentTimeMillis().toInt(),
+ contentIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+
+ val notificationChannel = NotificationChannel(
+ "chanel_terminal_bluetooth",
+ "chanel_terminal_bluetooth",
+ NotificationManager.IMPORTANCE_HIGH
+ )
+
+
+ val intent = Intent(this, CallBroadcastReceiver::class.java).apply {
+ action = "ACTION_EXIT"
+ }
+ val pendingIntent: PendingIntent =
+ PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ notificationManager.createNotificationChannel(notificationChannel)
+ mainNotification = NotificationCompat.Builder(
+ this, "chanel_terminal_bluetooth"
+ ).setSmallIcon(R.mipmap.ic_launcher)
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setOnlyAlertOnce(false)
+ .addAction(R.mipmap.ic_launcher, "Exit", pendingIntent)
+ .setContentIntent(contentPendingIntent)
+
+ Log.d("CallService", "Notification created")
+ }
+
+ companion object {
+ var isServiceRunning = false
+ const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_CHANNEL"
+ const val MAIN_NOTIFICATION_ID = 2323
+ fun startService(context: Context) {
+ Log.d("CallService", "Starting service")
+ Thread {
+ startIntent(context, Intent(context, CallService::class.java).apply {
+ action = CallServiceActions.START.name
+ })
+ }.start()
+ }
+
+ fun stopService(context: Context) {
+ Log.d("CallService", "Stopping service")
+ startIntent(context, Intent(context, CallService::class.java).apply {
+ action = CallServiceActions.STOP.name
+ })
+ }
+
+ private fun startIntent(context: Context, intent: Intent) {
+ context.startForegroundService(intent)
+ }
+ }
+
+ override fun onTransferEventToSocket(data: MessageModel) {
+ Log.d("CallService", "Transferring event to socket: ${data.type}")
+ socketClient.sendMessageToSocket(data)
+ }
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallServiceActions.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallServiceActions.kt
new file mode 100644
index 00000000..dcd0262d
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/service/CallServiceActions.kt
@@ -0,0 +1,5 @@
+package com.example.studybuddy.view.videocall.service
+
+enum class CallServiceActions {
+ START,STOP
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketClient.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketClient.kt
new file mode 100644
index 00000000..159ec328
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketClient.kt
@@ -0,0 +1,89 @@
+package com.example.studybuddy.view.videocall.socket
+
+import com.example.studybuddy.domain.model.MessageModel
+import com.google.gson.Gson
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.java_websocket.client.WebSocketClient
+import org.java_websocket.handshake.ServerHandshake
+import java.net.URI
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SocketClient @Inject constructor(
+ private val gson: Gson
+) {
+
+
+ private var socketEventListener: SocketEventListener? = null
+ fun setListener(messageInterface: SocketEventListener) {
+ this.socketEventListener = messageInterface
+ }
+
+ fun onStop() {
+ socketEventListener = null
+ runCatching { webSocket?.closeBlocking() }
+ }
+
+ private var webSocket: WebSocketClient? = null
+
+ init {
+ CoroutineScope(Dispatchers.IO).launch {
+ delay(1000)
+ initSocket()
+ }
+ }
+
+ private fun initSocket() {
+ //if you are using android emulator your local websocket address is going to be "ws://10.0.2.2:3000"
+ //if you are using your phone as emulator your local address, use cmd and then write ipconfig
+ // and get your ethernet ipv4 , mine is : "ws://192.168.1.3:3000"
+ //but if your websocket is deployed you add your websocket address here
+
+ webSocket = object : WebSocketClient(URI("wss://studdybuddyvideocall.onrender.com")) {
+ // webSocket = object : WebSocketClient(URI("ws://164.92.142.251:6000")) {
+ // webSocket = object : WebSocketClient(URI("ws://192.168.1.3:3000")) {
+ override fun onOpen(handshakedata: ServerHandshake?) {
+ socketEventListener?.onSocketOpened()
+ println("WebSocket connection established successfully!")
+ }
+
+ override fun onMessage(message: String?) {
+ try {
+ socketEventListener?.onNewMessage(
+ gson.fromJson(
+ message,
+ MessageModel::class.java
+ )
+ )
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ }
+
+ override fun onClose(code: Int, reason: String?, remote: Boolean) {
+ socketEventListener?.onSocketClosed()
+
+ }
+
+ override fun onError(ex: Exception?) {
+ }
+
+ }
+ webSocket?.connect()
+
+ }
+
+ fun sendMessageToSocket(messageModel: MessageModel) {
+ runCatching {
+ webSocket?.send(Gson().toJson(messageModel))
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventListener.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventListener.kt
new file mode 100644
index 00000000..cf2e963c
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventListener.kt
@@ -0,0 +1,9 @@
+package com.example.studybuddy.view.videocall.socket
+
+import com.example.studybuddy.domain.model.MessageModel
+
+interface SocketEventListener {
+ fun onNewMessage(message: MessageModel)
+ fun onSocketOpened()
+ fun onSocketClosed()
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventSender.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventSender.kt
new file mode 100644
index 00000000..05e50ab8
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEventSender.kt
@@ -0,0 +1,43 @@
+package com.example.studybuddy.view.videocall.socket
+
+
+import com.example.studybuddy.StudyBuddyApp
+import com.example.studybuddy.domain.model.MessageModel
+import javax.inject.Inject
+
+class SocketEventSender @Inject constructor(
+ private val socketClient: SocketClient
+) {
+
+ private var username = StudyBuddyApp.username
+
+ fun storeUser() {
+ socketClient.sendMessageToSocket(
+ MessageModel(type = SocketEvents.StoreUser, name = username)
+ )
+ }
+
+ fun createRoom(roomName: String) {
+ socketClient.sendMessageToSocket(
+ MessageModel(type = SocketEvents.CreateRoom, data = roomName, name = username)
+ )
+ }
+
+ fun joinRoom(roomName: String) {
+ socketClient.sendMessageToSocket(
+ MessageModel(type = SocketEvents.JoinRoom, data = roomName, name = username)
+ )
+ }
+
+ fun leaveAllRooms() {
+ socketClient.sendMessageToSocket(
+ MessageModel(type = SocketEvents.LeaveAllRooms, name = username)
+ )
+ }
+
+ fun startCall(target: String) {
+ socketClient.sendMessageToSocket(
+ MessageModel(type = SocketEvents.StartCall, name = username, target = target)
+ )
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEvents.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEvents.kt
new file mode 100644
index 00000000..81b95acc
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/socket/SocketEvents.kt
@@ -0,0 +1,6 @@
+package com.example.studybuddy.view.videocall.socket
+
+enum class SocketEvents {
+ StoreUser,CreateRoom,JoinRoom,RoomStatus,LeaveAllRooms,
+ Offer, Answer, Ice,NewSession,StartCall
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/viewmodel/MainViewModel.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/viewmodel/MainViewModel.kt
new file mode 100644
index 00000000..302a378e
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/viewmodel/MainViewModel.kt
@@ -0,0 +1,116 @@
+package com.example.studybuddy.view.videocall.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.studybuddy.domain.model.RoomModel
+import com.example.studybuddy.view.videocall.service.CallService
+import com.example.studybuddy.view.videocall.service.CallServiceActions
+import com.example.studybuddy.view.videocall.socket.SocketEventSender
+import com.example.studybuddy.view.videocall.webrtc.WebRTCFactory
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import org.webrtc.MediaStream
+import org.webrtc.SurfaceViewRenderer
+import javax.inject.Inject
+
+@HiltViewModel
+@SuppressLint("StaticFieldLeak")
+class MainViewModel @Inject constructor(
+ private val context: Context,
+ private val eventSender: SocketEventSender,
+ private val webRTCFactory: WebRTCFactory
+) : ViewModel() {
+
+ private lateinit var callService: CallService
+ private var isBound = false
+
+ //states
+ var roomsState: MutableStateFlow?> = MutableStateFlow(null)
+ var mediaStreamsState: MutableStateFlow?> = MutableStateFlow(
+ hashMapOf()
+ )
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName?, service: IBinder?) {
+ val binder = service as CallService.LocalBinder
+ callService = binder.getService()
+ isBound = true
+ handleServiceBound()
+ Log.d("MainViewModel", "Service Bound Successfully")
+ }
+
+ override fun onServiceDisconnected(arg0: ComponentName) {
+ isBound = false
+ Log.d("MainViewModel", "Service Disconnected")
+ }
+ }
+
+ private fun handleServiceBound() {
+ callService.roomsState.onEach { rooms ->
+ roomsState.emit(rooms)
+ }.launchIn(viewModelScope)
+
+ callService.mediaStreamsState.onEach { mediaStreams ->
+ // Directly propagate the state without setting a new value
+ mediaStreamsState.emit(mediaStreams)
+ }.launchIn(viewModelScope)
+ }
+
+ fun onCreateRoomClicked(roomName: String) {
+ eventSender.createRoom(roomName)
+ }
+
+ fun onRoomJoined(roomName: String, view:SurfaceViewRenderer) {
+ if (isBound) { // Ensure service is bound
+ callService.initializeSurface(view)
+ eventSender.joinRoom(roomName)
+ } else {
+ Log.e("MainViewModel", "CallService is not bound when trying to join room.")
+ // Handle the case where the service is not yet bound
+ }
+ }
+
+ fun init() {
+ Intent(context, CallService::class.java).apply {
+ action = CallServiceActions.START.name
+ }.also { intent ->
+ CallService.startService(context)
+ context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+
+ }
+
+
+ override fun onCleared() {
+ if (isBound) {
+ context.unbindService(serviceConnection)
+ isBound = false
+ Log.d("MainViewModel", "Service Unbound on ViewModel cleared.")
+ }
+ super.onCleared()
+ }
+
+ fun onLeaveConferenceClicked() {
+ if (isBound) {
+ eventSender.leaveAllRooms()
+ callService.leaveRoom()
+ } else {
+ Log.e("MainViewModel", "CallService is not bound when trying to leave room.")
+ }
+ }
+
+ fun initRemoteSurfaceView(view: SurfaceViewRenderer) {
+ webRTCFactory.initRemoteSurfaceView(view)
+ }
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/BluetoothManager.java b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/BluetoothManager.java
new file mode 100644
index 00000000..89ed425d
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/BluetoothManager.java
@@ -0,0 +1,439 @@
+package com.example.studybuddy.view.videocall.webrtc;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+
+import org.webrtc.ThreadUtils;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * AppRTCProximitySensor manages functions related to Bluetoth devices in the
+ * AppRTC demo.
+ */
+@SuppressLint("MissingPermission")
+public class BluetoothManager {
+ private static final String TAG = "AppRTCBluetoothManager";
+
+ // Timeout interval for starting or stopping audio to a Bluetooth SCO device.
+ private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
+ // Maximum number of SCO connection attempts.
+ private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
+ private final Context apprtcContext;
+ private final RTCAudioManager apprtcAudioManager;
+ private final android.media.AudioManager audioManager;
+ private final Handler handler;
+ private final BluetoothProfile.ServiceListener bluetoothServiceListener;
+ private final BroadcastReceiver bluetoothHeadsetReceiver;
+ private int scoConnectionAttempts;
+ private State bluetoothState;
+ private BluetoothAdapter bluetoothAdapter;
+ private BluetoothHeadset bluetoothHeadset;
+ private BluetoothDevice bluetoothDevice;
+ // Runs when the Bluetooth timeout expires. We use that timeout after calling
+ // startScoAudio() or stopScoAudio() because we're not guaranteed to get a
+ // callback after those calls.
+ private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout;
+
+ private BluetoothManager(Context context, RTCAudioManager audioManager) {
+ ThreadUtils.checkIsOnMainThread();
+ apprtcContext = context;
+ apprtcAudioManager = audioManager;
+ this.audioManager = getAudioManager(context);
+ bluetoothState = State.UNINITIALIZED;
+ bluetoothServiceListener = new BluetoothServiceListener();
+ bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Construction.
+ */
+ static BluetoothManager create(Context context, RTCAudioManager audioManager) {
+ return new BluetoothManager(context, audioManager);
+ }
+
+ /**
+ * Returns the internal state.
+ */
+ public State getState() {
+ ThreadUtils.checkIsOnMainThread();
+ return bluetoothState;
+ }
+
+ /**
+ * Activates components required to detect Bluetooth devices and to enable
+ * BT SCO (audio is routed via BT SCO) for the headset profile. The end
+ * state will be HEADSET_UNAVAILABLE but a state machine has started which
+ * will start a state change sequence where the final outcome depends on
+ * if/when the BT headset is enabled.
+ * Example of state change sequence when start() is called while BT device
+ * is connected and enabled:
+ * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
+ * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
+ * Note that the AppRTCAudioManager is also involved in driving this state
+ * change.
+ */
+ @SuppressLint("MissingPermission")
+ public void start() {
+ ThreadUtils.checkIsOnMainThread();
+ if (!hasPermission()) {
+ return;
+ }
+ if (bluetoothState != State.UNINITIALIZED) {
+ return;
+ }
+ bluetoothHeadset = null;
+ bluetoothDevice = null;
+ scoConnectionAttempts = 0;
+ // Get a handle to the default local Bluetooth adapter.
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (bluetoothAdapter == null) {
+ return;
+ }
+ // Ensure that the device supports use of BT SCO audio for off call use cases.
+ if (!audioManager.isBluetoothScoAvailableOffCall()) {
+ return;
+ }
+ logBluetoothAdapterInfo(bluetoothAdapter);
+ // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
+ // Hands-Free) proxy object and install a listener.
+ if (!getBluetoothProfileProxy(apprtcContext, bluetoothServiceListener)) {
+ return;
+ }
+ // Register receivers for BluetoothHeadset change notifications.
+ IntentFilter bluetoothHeadsetFilter = new IntentFilter();
+ // Register receiver for change in connection state of the Headset profile.
+ bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ // Register receiver for change in audio connection state of the Headset profile.
+ bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+ registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
+ bluetoothState = State.HEADSET_UNAVAILABLE;
+ }
+
+ /**
+ * Stops and closes all components related to Bluetooth audio.
+ */
+ public void stop() {
+ ThreadUtils.checkIsOnMainThread();
+ if (bluetoothAdapter == null) {
+ return;
+ }
+ // Stop BT SCO connection with remote device if needed.
+ stopScoAudio();
+ // Close down remaining BT resources.
+ if (bluetoothState == State.UNINITIALIZED) {
+ return;
+ }
+ unregisterReceiver(bluetoothHeadsetReceiver);
+ cancelTimer();
+ if (bluetoothHeadset != null) {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
+ bluetoothHeadset = null;
+ }
+ bluetoothAdapter = null;
+ bluetoothDevice = null;
+ bluetoothState = State.UNINITIALIZED;
+ }
+
+ /**
+ * Starts Bluetooth SCO connection with remote device.
+ * Note that the phone application always has the priority on the usage of the SCO connection
+ * for telephony. If this method is called while the phone is in call it will be ignored.
+ * Similarly, if a call is received or sent while an application is using the SCO connection,
+ * the connection will be lost for the application and NOT returned automatically when the call
+ * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
+ * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
+ * audio connection is established.
+ * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
+ * higher. It might be required to initiates a virtual voice call since many devices do not
+ * accept SCO audio without a "call".
+ */
+ public boolean startScoAudio() {
+ ThreadUtils.checkIsOnMainThread();
+ if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
+ return false;
+ }
+ if (bluetoothState != State.HEADSET_AVAILABLE) {
+ return false;
+ }
+ // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
+ // The SCO connection establishment can take several seconds, hence we cannot rely on the
+ // connection to be available when the method returns but instead register to receive the
+ // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
+ bluetoothState = State.SCO_CONNECTING;
+ audioManager.startBluetoothSco();
+ audioManager.setBluetoothScoOn(true);
+ scoConnectionAttempts++;
+ startTimer();
+ return true;
+ }
+
+ /**
+ * Stops Bluetooth SCO connection with remote device.
+ */
+ public void stopScoAudio() {
+ ThreadUtils.checkIsOnMainThread();
+ if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
+ return;
+ }
+ cancelTimer();
+ audioManager.stopBluetoothSco();
+ audioManager.setBluetoothScoOn(false);
+ bluetoothState = State.SCO_DISCONNECTING;
+ }
+
+ /**
+ * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
+ * Service via IPC) to update the list of connected devices for the HEADSET
+ * profile. The internal state will change to HEADSET_UNAVAILABLE or to
+ * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
+ * device if available.
+ */
+ public void updateDevice() {
+ if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
+ return;
+ }
+ // Get connected devices for the headset profile. Returns the set of
+ // devices which are in state STATE_CONNECTED. The BluetoothDevice class
+ // is just a thin wrapper for a Bluetooth hardware address.
+ List devices = bluetoothHeadset.getConnectedDevices();
+ if (devices.isEmpty()) {
+ bluetoothDevice = null;
+ bluetoothState = State.HEADSET_UNAVAILABLE;
+ } else {
+ // Always use first device in list. Android only supports one device.
+ bluetoothDevice = devices.get(0);
+ bluetoothState = State.HEADSET_AVAILABLE;
+ }
+ }
+
+ /**
+ * Stubs for test mocks.
+ */
+ protected android.media.AudioManager getAudioManager(Context context) {
+ return (android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ apprtcContext.registerReceiver(receiver, filter);
+ }
+
+ protected void unregisterReceiver(BroadcastReceiver receiver) {
+ apprtcContext.unregisterReceiver(receiver);
+ }
+
+ protected boolean getBluetoothProfileProxy(Context context, BluetoothProfile.ServiceListener listener) {
+ return bluetoothAdapter.getProfileProxy(context, listener, BluetoothProfile.HEADSET);
+ }
+
+ protected boolean hasPermission() {
+ return apprtcContext.checkPermission(android.Manifest.permission.BLUETOOTH, Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Logs the state of the local Bluetooth adapter.
+ */
+ @SuppressLint("HardwareIds")
+ protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
+ // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
+ Set pairedDevices = localAdapter.getBondedDevices();
+
+ }
+
+ /**
+ * Ensures that the audio manager updates its list of available audio devices.
+ */
+ private void updateAudioDeviceState() {
+ ThreadUtils.checkIsOnMainThread();
+ apprtcAudioManager.updateAudioDeviceState();
+ }
+
+ /**
+ * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
+ */
+ private void startTimer() {
+ ThreadUtils.checkIsOnMainThread();
+ handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
+ }
+
+ /**
+ * Cancels any outstanding timer tasks.
+ */
+ private void cancelTimer() {
+ ThreadUtils.checkIsOnMainThread();
+ handler.removeCallbacks(bluetoothTimeoutRunnable);
+ }
+
+ /**
+ * Called when start of the BT SCO channel takes too long time. Usually
+ * happens when the BT device has been turned on during an ongoing call.
+ */
+ private void bluetoothTimeout() {
+ ThreadUtils.checkIsOnMainThread();
+ if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
+ return;
+ }
+ if (bluetoothState != State.SCO_CONNECTING) {
+ return;
+ }
+ // Bluetooth SCO should be connecting; check the latest result.
+ boolean scoConnected = false;
+ List devices = bluetoothHeadset.getConnectedDevices();
+ if (devices.size() > 0) {
+ bluetoothDevice = devices.get(0);
+ if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
+ scoConnected = true;
+ }
+ }
+ if (scoConnected) {
+ // We thought BT had timed out, but it's actually on; updating state.
+ bluetoothState = State.SCO_CONNECTED;
+ scoConnectionAttempts = 0;
+ } else {
+ // Give up and "cancel" our request by calling stopBluetoothSco().
+ stopScoAudio();
+ }
+ updateAudioDeviceState();
+ }
+
+ /**
+ * Checks whether audio uses Bluetooth SCO.
+ */
+ private boolean isScoOn() {
+ return audioManager.isBluetoothScoOn();
+ }
+
+ /**
+ * Converts BluetoothAdapter states into local string representations.
+ */
+ private String stateToString(int state) {
+ switch (state) {
+ case BluetoothAdapter.STATE_DISCONNECTED:
+ return "DISCONNECTED";
+ case BluetoothAdapter.STATE_CONNECTED:
+ return "CONNECTED";
+ case BluetoothAdapter.STATE_CONNECTING:
+ return "CONNECTING";
+ case BluetoothAdapter.STATE_DISCONNECTING:
+ return "DISCONNECTING";
+ case BluetoothAdapter.STATE_OFF:
+ return "OFF";
+ case BluetoothAdapter.STATE_ON:
+ return "ON";
+ case BluetoothAdapter.STATE_TURNING_OFF:
+ // Indicates the local Bluetooth adapter is turning off. Local clients should immediately
+ // attempt graceful disconnection of any remote links.
+ return "TURNING_OFF";
+ case BluetoothAdapter.STATE_TURNING_ON:
+ // Indicates the local Bluetooth adapter is turning on. However local clients should wait
+ // for STATE_ON before attempting to use the adapter.
+ return "TURNING_ON";
+ default:
+ return "INVALID";
+ }
+ }
+
+ // Bluetooth connection state.
+ public enum State {
+ // Bluetooth is not available; no adapter or Bluetooth is off.
+ UNINITIALIZED, // Bluetooth error happened when trying to start Bluetooth.
+ ERROR, // Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
+ // SCO is not started or disconnected.
+ HEADSET_UNAVAILABLE, // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset
+ // present, but SCO is not started or disconnected.
+ HEADSET_AVAILABLE, // Bluetooth audio SCO connection with remote device is closing.
+ SCO_DISCONNECTING, // Bluetooth audio SCO connection with remote device is initiated.
+ SCO_CONNECTING, // Bluetooth audio SCO connection with remote device is established.
+ SCO_CONNECTED
+ }
+
+ /**
+ * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
+ * connected to or disconnected from the service.
+ */
+ private class BluetoothServiceListener implements BluetoothProfile.ServiceListener {
+ @Override
+ // Called to notify the client when the proxy object has been connected to the service.
+ // Once we have the profile proxy object, we can use it to monitor the state of the
+ // connection and perform other operations that are relevant to the headset profile.
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
+ return;
+ }
+ // Android only supports one connected Bluetooth Headset at a time.
+ bluetoothHeadset = (BluetoothHeadset) proxy;
+ updateAudioDeviceState();
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
+ return;
+ }
+ stopScoAudio();
+ bluetoothHeadset = null;
+ bluetoothDevice = null;
+ bluetoothState = State.HEADSET_UNAVAILABLE;
+ updateAudioDeviceState();
+ }
+ }
+
+ // Intent broadcast receiver which handles changes in Bluetooth device availability.
+ // Detects headset changes and Bluetooth SCO state changes.
+ private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (bluetoothState == State.UNINITIALIZED) {
+ return;
+ }
+ final String action = intent.getAction();
+ // Change in connection state of the Headset profile. Note that the
+ // change does not tell us anything about whether we're streaming
+ // audio to BT over SCO. Typically received when user turns on a BT
+ // headset while audio is active using another audio device.
+ if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+ final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
+ if (state == BluetoothHeadset.STATE_CONNECTED) {
+ scoConnectionAttempts = 0;
+ updateAudioDeviceState();
+ } else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
+ // Bluetooth is probably powered off during the call.
+ stopScoAudio();
+ updateAudioDeviceState();
+ }
+ // Change in the audio (SCO) connection state of the Headset profile.
+ // Typically received after call to startScoAudio() has finalized.
+ } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+ final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
+ cancelTimer();
+ if (bluetoothState == State.SCO_CONNECTING) {
+ bluetoothState = State.SCO_CONNECTED;
+ scoConnectionAttempts = 0;
+ updateAudioDeviceState();
+ } else {
+ }
+ } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
+ } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
+ if (isInitialStickyBroadcast()) {
+ return;
+ }
+ updateAudioDeviceState();
+ }
+ }
+ }
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/LocalStreamListener.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/LocalStreamListener.kt
new file mode 100644
index 00000000..3ca9dc4d
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/LocalStreamListener.kt
@@ -0,0 +1,7 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import org.webrtc.MediaStream
+
+interface LocalStreamListener {
+ fun onLocalStreamReady(mediaStream: MediaStream)
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MyPeerObserver.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MyPeerObserver.kt
new file mode 100644
index 00000000..bf520212
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MyPeerObserver.kt
@@ -0,0 +1,39 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import org.webrtc.*
+
+open class MyPeerObserver : PeerConnection.Observer{
+ override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
+
+ }
+
+ override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
+ }
+
+ override fun onIceConnectionReceivingChange(p0: Boolean) {
+ }
+
+ override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
+ }
+
+ override fun onIceCandidate(p0: IceCandidate?) {
+ }
+
+ override fun onIceCandidatesRemoved(p0: Array?) {
+ }
+
+ override fun onAddStream(p0: MediaStream?) {
+ }
+
+ override fun onRemoveStream(p0: MediaStream?) {
+ }
+
+ override fun onDataChannel(p0: DataChannel?) {
+ }
+
+ override fun onRenegotiationNeeded() {
+ }
+
+ override fun onAddTrack(p0: RtpReceiver?, p1: Array?) {
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MySdpObserver.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MySdpObserver.kt
new file mode 100644
index 00000000..78d1d381
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/MySdpObserver.kt
@@ -0,0 +1,18 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import org.webrtc.SdpObserver
+import org.webrtc.SessionDescription
+
+open class MySdpObserver : SdpObserver{
+ override fun onCreateSuccess(desc: SessionDescription?) {
+ }
+
+ override fun onSetSuccess() {
+ }
+
+ override fun onCreateFailure(p0: String?) {
+ }
+
+ override fun onSetFailure(p0: String?) {
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/ProximitySensor.java b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/ProximitySensor.java
new file mode 100644
index 00000000..dbb71737
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/ProximitySensor.java
@@ -0,0 +1,140 @@
+package com.example.studybuddy.view.videocall.webrtc;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+import org.webrtc.ThreadUtils;
+
+/**
+ * AppRTCProximitySensor manages functions related to the proximity sensor in
+ * the AppRTC demo.
+ * On most device, the proximity sensor is implemented as a boolean-sensor.
+ * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
+ * value i.e. the LUX value of the light sensor is compared with a threshold.
+ * A LUX-value more than the threshold means the proximity sensor returns "FAR".
+ * Anything less than the threshold value and the sensor returns "NEAR".
+ */
+@SuppressLint("MissingPermission")
+public class ProximitySensor implements SensorEventListener {
+ private static final String TAG = ProximitySensor.class.getSimpleName();
+
+ // This class should be created, started and stopped on one thread
+ // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is
+ // the case. Only active when |DEBUG| is set to true.
+ private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
+
+ private final Runnable onSensorStateListener;
+ private final SensorManager sensorManager;
+ private Sensor proximitySensor = null;
+ private boolean lastStateReportIsNear = false;
+
+ private ProximitySensor(Context context, Runnable sensorStateListener) {
+ onSensorStateListener = sensorStateListener;
+ sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
+ }
+
+ /**
+ * Construction
+ */
+ static ProximitySensor create(Context context, Runnable sensorStateListener) {
+ return new ProximitySensor(context, sensorStateListener);
+ }
+
+ /**
+ * Activate the proximity sensor. Also do initialization if called for the
+ * first time.
+ */
+ public boolean start() {
+ threadChecker.checkIsOnValidThread();
+ if (!initDefaultSensor()) {
+ // Proximity sensor is not supported on this device.
+ return false;
+ }
+ sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+ return true;
+ }
+
+ /**
+ * Deactivate the proximity sensor.
+ */
+ public void stop() {
+ threadChecker.checkIsOnValidThread();
+ if (proximitySensor == null) {
+ return;
+ }
+ sensorManager.unregisterListener(this, proximitySensor);
+ }
+
+ /**
+ * Getter for last reported state. Set to true if "near" is reported.
+ */
+ public boolean sensorReportsNearState() {
+ threadChecker.checkIsOnValidThread();
+ return lastStateReportIsNear;
+ }
+
+ @Override
+ public final void onAccuracyChanged(Sensor sensor, int accuracy) {
+ threadChecker.checkIsOnValidThread();
+
+ }
+
+ @Override
+ public final void onSensorChanged(SensorEvent event) {
+ threadChecker.checkIsOnValidThread();
+ // As a best practice; do as little as possible within this method and
+ // avoid blocking.
+ float distanceInCentimeters = event.values[0];
+ lastStateReportIsNear = distanceInCentimeters < proximitySensor.getMaximumRange();
+
+ // Report about new state to listening client. Client can then call
+ // sensorReportsNearState() to query the current state (NEAR or FAR).
+ if (onSensorStateListener != null) {
+ onSensorStateListener.run();
+ }
+
+ }
+
+ /**
+ * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
+ * does not support this type of sensor and false will be returned in such
+ * cases.
+ */
+ private boolean initDefaultSensor() {
+ if (proximitySensor != null) {
+ return true;
+ }
+ proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ if (proximitySensor == null) {
+ return false;
+ }
+ logProximitySensorInfo();
+ return true;
+ }
+
+ /**
+ * Helper method for logging information about the proximity sensor.
+ */
+ private void logProximitySensorInfo() {
+ if (proximitySensor == null) {
+ return;
+ }
+ StringBuilder info = new StringBuilder("Proximity sensor: ");
+ info.append("name=").append(proximitySensor.getName());
+ info.append(", vendor: ").append(proximitySensor.getVendor());
+ info.append(", power: ").append(proximitySensor.getPower());
+ info.append(", resolution: ").append(proximitySensor.getResolution());
+ info.append(", max range: ").append(proximitySensor.getMaximumRange());
+ info.append(", min delay: ").append(proximitySensor.getMinDelay());
+ // Added in API level 20.
+ info.append(", type: ").append(proximitySensor.getStringType());
+ // Added in API level 21.
+ info.append(", max delay: ").append(proximitySensor.getMaxDelay());
+ info.append(", reporting mode: ").append(proximitySensor.getReportingMode());
+ info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor());
+ }
+}
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCAudioManager.java b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCAudioManager.java
new file mode 100644
index 00000000..4d84a1cc
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCAudioManager.java
@@ -0,0 +1,523 @@
+package com.example.studybuddy.view.videocall.webrtc;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.preference.PreferenceManager;
+
+import org.webrtc.ThreadUtils;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * AppRTCAudioManager manages all audio related parts of the AppRTC demo.
+ */
+@SuppressLint("MissingPermission")
+public class RTCAudioManager {
+ private static final String TAG = RTCAudioManager.class.getSimpleName();
+ private static final String SPEAKERPHONE_AUTO = "auto";
+ private static final String SPEAKERPHONE_TRUE = "true";
+ private static final String SPEAKERPHONE_FALSE = "false";
+ private final Context apprtcContext;
+ // Contains speakerphone setting: auto, true or false
+ private final String useSpeakerphone;
+ // Handles all tasks related to Bluetooth headset devices.
+ private final BluetoothManager bluetoothManager;
+ private final android.media.AudioManager audioManager;
+ private AudioManagerEvents audioManagerEvents;
+ private AudioManagerState amState;
+ private int savedAudioMode = android.media.AudioManager.MODE_INVALID;
+ private boolean savedIsSpeakerPhoneOn = false;
+ private boolean savedIsMicrophoneMute = false;
+ private boolean hasWiredHeadset = false;
+ // Default audio device; speaker phone for video calls or earpiece for audio
+ // only calls.
+ private AudioDevice defaultAudioDevice;
+ // Contains the currently selected audio device.
+ // This device is changed automatically using a certain scheme where e.g.
+ // a wired headset "wins" over speaker phone. It is also possible for a
+ // user to explicitly select a device (and overrid any predefined scheme).
+ // See |userSelectedAudioDevice| for details.
+ private AudioDevice selectedAudioDevice;
+ // Contains the user-selected audio device which overrides the predefined
+ // selection scheme.
+ // TODO(henrika): always set to AudioDevice.NONE today. Add support for
+ // explicit selection based on choice by userSelectedAudioDevice.
+ private AudioDevice userSelectedAudioDevice;
+ // Proximity sensor object. It measures the proximity of an object in cm
+ // relative to the view screen of a device and can therefore be used to
+ // assist device switching (close to ear <=> use headset earpiece if
+ // available, far from ear <=> use speaker phone).
+ private ProximitySensor proximitySensor;
+ // Contains a list of available audio devices. A Set collection is used to
+ // avoid duplicate elements.
+ private Set audioDevices = new HashSet<>();
+ // Broadcast receiver for wired headset intent broadcasts.
+ private final BroadcastReceiver wiredHeadsetReceiver;
+ // Callback method for changes in audio focus.
+ private android.media.AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
+
+ private RTCAudioManager(Context context) {
+ ThreadUtils.checkIsOnMainThread();
+ apprtcContext = context;
+ audioManager = ((android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
+ bluetoothManager = BluetoothManager.create(context, this);
+ wiredHeadsetReceiver = new WiredHeadsetReceiver();
+ amState = AudioManagerState.UNINITIALIZED;
+
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ useSpeakerphone = sharedPreferences.getString("speakerphone_preference", "auto");
+ if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) {
+ defaultAudioDevice = AudioDevice.EARPIECE;
+ } else {
+ defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ }
+
+ // Create and initialize the proximity sensor.
+ // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
+ // Note that, the sensor will not be active until start() has been called.
+ proximitySensor = ProximitySensor.create(context, new Runnable() {
+ // This method will be called each time a state change is detected.
+ // Example: user holds his hand over the device (closer than ~5 cm),
+ // or removes his hand from the device.
+ public void run() {
+ onProximitySensorChangedState();
+ }
+ });
+
+ }
+
+ /**
+ * Construction.
+ */
+ public static RTCAudioManager create(Context context) {
+ return new RTCAudioManager(context);
+ }
+
+ /**
+ * This method is called when the proximity sensor reports a state change,
+ * e.g. from "NEAR to FAR" or from "FAR to NEAR".
+ */
+ private void onProximitySensorChangedState() {
+ if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) {
+ return;
+ }
+
+ // The proximity sensor should only be activated when there are exactly two
+ // available audio devices.
+ if (audioDevices.size() == 2 && audioDevices.contains(AudioDevice.EARPIECE) && audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
+ if (proximitySensor.sensorReportsNearState()) {
+ // Sensor reports that a "handset is being held up to a person's ear",
+ // or "something is covering the light sensor".
+ setAudioDeviceInternal(AudioDevice.EARPIECE);
+ } else {
+ // Sensor reports that a "handset is removed from a person's ear", or
+ // "the light sensor is no longer covered".
+ setAudioDeviceInternal(AudioDevice.SPEAKER_PHONE);
+ }
+ }
+ }
+
+ public void start(AudioManagerEvents audioManagerEvents) {
+ ThreadUtils.checkIsOnMainThread();
+ if (amState == AudioManagerState.RUNNING) {
+ return;
+ }
+ // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED.
+
+ this.audioManagerEvents = audioManagerEvents;
+ amState = AudioManagerState.RUNNING;
+
+ // Store current audio state so we can restore it when stop() is called.
+ savedAudioMode = audioManager.getMode();
+ savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
+ savedIsMicrophoneMute = audioManager.isMicrophoneMute();
+ hasWiredHeadset = hasWiredHeadset();
+
+ // Create an AudioManager.OnAudioFocusChangeListener instance.
+ // Called on the listener to notify if the audio focus for this listener has been changed.
+// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
+// and whether that loss is transient, or whether the new focus holder will hold it for an
+// unknown amount of time.
+// TODO(henrika): possibly extend support of handling audio-focus changes. Only contains
+// logging for now.
+ audioFocusChangeListener = focusChange -> {
+ String typeOfChange = "AUDIOFOCUS_NOT_DEFINED";
+ switch (focusChange) {
+ case android.media.AudioManager.AUDIOFOCUS_GAIN:
+ typeOfChange = "AUDIOFOCUS_GAIN";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
+ typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
+ typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
+ typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_LOSS:
+ typeOfChange = "AUDIOFOCUS_LOSS";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
+ break;
+ case android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
+ break;
+ default:
+ typeOfChange = "AUDIOFOCUS_INVALID";
+ break;
+ }
+ };
+
+ // Request audio playout focus (without ducking) and install listener for changes in focus.
+ int result = audioManager.requestAudioFocus(audioFocusChangeListener, android.media.AudioManager.STREAM_VOICE_CALL, android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+
+ // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
+ // required to be in this mode when playout and/or recording starts for
+ // best possible VoIP performance.
+ audioManager.setMode(android.media.AudioManager.MODE_IN_COMMUNICATION);
+
+ // Always disable microphone mute during a WebRTC call.
+ setMicrophoneMute(false);
+
+ // Set initial device states.
+ userSelectedAudioDevice = AudioDevice.NONE;
+ selectedAudioDevice = AudioDevice.NONE;
+ audioDevices.clear();
+
+ // Initialize and start Bluetooth if a BT device is available or initiate
+ // detection of new (enabled) BT devices.
+ bluetoothManager.start();
+
+ // Do initial selection of audio device. This setting can later be changed
+ // either by adding/removing a BT or wired headset or by covering/uncovering
+ // the proximity sensor.
+ updateAudioDeviceState();
+
+ // Register receiver for broadcast intents related to adding/removing a
+ // wired headset.
+ registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+ }
+
+ @SuppressLint("WrongConstant")
+ public void stop() {
+ ThreadUtils.checkIsOnMainThread();
+ if (amState != AudioManagerState.RUNNING) {
+ return;
+ }
+ amState = AudioManagerState.UNINITIALIZED;
+
+ unregisterReceiver(wiredHeadsetReceiver);
+
+ bluetoothManager.stop();
+
+ // Restore previously stored audio states.
+ setSpeakerphoneOn(savedIsSpeakerPhoneOn);
+ setMicrophoneMute(savedIsMicrophoneMute);
+ audioManager.setMode(savedAudioMode);
+
+ // Abandon audio focus. Gives the previous focus owner, if any, focus.
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ audioFocusChangeListener = null;
+
+ if (proximitySensor != null) {
+ proximitySensor.stop();
+ proximitySensor = null;
+ }
+
+ audioManagerEvents = null;
+ }
+
+ /**
+ * Changes selection of the currently active audio device.
+ */
+ private void setAudioDeviceInternal(AudioDevice device) {
+ switch (device) {
+ case SPEAKER_PHONE:
+ setSpeakerphoneOn(true);
+ break;
+ case EARPIECE:
+ case BLUETOOTH:
+ case WIRED_HEADSET:
+ setSpeakerphoneOn(false);
+ break;
+ default:
+ break;
+ }
+ selectedAudioDevice = device;
+ }
+
+ /**
+ * Changes default audio device.
+ * TODO(henrika): add usage of this method in the AppRTCMobile client.
+ */
+ public void setDefaultAudioDevice(AudioDevice defaultDevice) {
+ ThreadUtils.checkIsOnMainThread();
+ switch (defaultDevice) {
+ case SPEAKER_PHONE:
+ defaultAudioDevice = defaultDevice;
+ break;
+ case EARPIECE:
+ if (hasEarpiece()) {
+ defaultAudioDevice = defaultDevice;
+ } else {
+ defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+ }
+ break;
+ default:
+ break;
+ }
+ updateAudioDeviceState();
+ }
+
+ /**
+ * Changes selection of the currently active audio device.
+ */
+ public void selectAudioDevice(AudioDevice device) {
+ ThreadUtils.checkIsOnMainThread();
+ if (!audioDevices.contains(device)) {
+ }
+ userSelectedAudioDevice = device;
+ updateAudioDeviceState();
+ }
+
+ /**
+ * Returns current set of available/selectable audio devices.
+ */
+ public Set getAudioDevices() {
+ ThreadUtils.checkIsOnMainThread();
+ return Collections.unmodifiableSet(new HashSet<>(audioDevices));
+ }
+
+ /**
+ * Returns the currently selected audio device.
+ */
+ public AudioDevice getSelectedAudioDevice() {
+ ThreadUtils.checkIsOnMainThread();
+ return selectedAudioDevice;
+ }
+
+ /**
+ * Helper method for receiver registration.
+ */
+ private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ apprtcContext.registerReceiver(receiver, filter);
+ }
+
+ /**
+ * Helper method for unregistration of an existing receiver.
+ */
+ private void unregisterReceiver(BroadcastReceiver receiver) {
+ apprtcContext.unregisterReceiver(receiver);
+ }
+
+ /**
+ * Sets the speaker phone mode.
+ */
+ private void setSpeakerphoneOn(boolean on) {
+ boolean wasOn = audioManager.isSpeakerphoneOn();
+ if (wasOn == on) {
+ return;
+ }
+ audioManager.setSpeakerphoneOn(on);
+ }
+
+ /**
+ * Sets the microphone mute state.
+ */
+ private void setMicrophoneMute(boolean on) {
+ boolean wasMuted = audioManager.isMicrophoneMute();
+ if (wasMuted == on) {
+ return;
+ }
+ audioManager.setMicrophoneMute(on);
+ }
+
+ /**
+ * Gets the current earpiece state.
+ */
+ private boolean hasEarpiece() {
+ return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+ }
+
+ /**
+ * Checks whether a wired headset is connected or not.
+ * This is not a valid indication that audio playback is actually over
+ * the wired headset as audio routing depends on other conditions. We
+ * only use it as an early indicator (during initialization) of an attached
+ * wired headset.
+ */
+ @Deprecated
+ private boolean hasWiredHeadset() {
+ @SuppressLint("WrongConstant") final AudioDeviceInfo[] devices = audioManager.getDevices(android.media.AudioManager.GET_DEVICES_ALL);
+ for (AudioDeviceInfo device : devices) {
+ final int type = device.getType();
+ if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+ return true;
+ } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Updates list of possible audio devices and make new device selection.
+ * TODO(henrika): add unit test to verify all state transitions.
+ */
+ public void updateAudioDeviceState() {
+ ThreadUtils.checkIsOnMainThread();
+
+ // Check if any Bluetooth headset is connected. The internal BT state will
+ // change accordingly.
+ // TODO(henrika): perhaps wrap required state into BT manager.
+ if (bluetoothManager.getState() == BluetoothManager.State.HEADSET_AVAILABLE || bluetoothManager.getState() == BluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == BluetoothManager.State.SCO_DISCONNECTING) {
+ bluetoothManager.updateDevice();
+ }
+
+ // Update the set of available audio devices.
+ Set newAudioDevices = new HashSet<>();
+
+ if (bluetoothManager.getState() == BluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == BluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == BluetoothManager.State.HEADSET_AVAILABLE) {
+ newAudioDevices.add(AudioDevice.BLUETOOTH);
+ }
+
+ if (hasWiredHeadset) {
+ // If a wired headset is connected, then it is the only possible option.
+ newAudioDevices.add(AudioDevice.WIRED_HEADSET);
+ } else {
+ // No wired headset, hence the audio-device list can contain speaker
+ // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
+ newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
+ if (hasEarpiece()) {
+ newAudioDevices.add(AudioDevice.EARPIECE);
+ }
+ }
+ // Store state which is set to true if the device list has changed.
+ boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
+ // Update the existing audio device set.
+ audioDevices = newAudioDevices;
+ // Correct user selected audio devices if needed.
+ if (bluetoothManager.getState() == BluetoothManager.State.HEADSET_UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
+ // If BT is not available, it can't be the user selection.
+ userSelectedAudioDevice = AudioDevice.NONE;
+ }
+ if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
+ // If user selected speaker phone, but then plugged wired headset then make
+ // wired headset as user selected device.
+ userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
+ }
+ if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
+ // If user selected wired headset, but then unplugged wired headset then make
+ // speaker phone as user selected device.
+ userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
+ }
+
+ // Need to start Bluetooth if it is available and user either selected it explicitly or
+ // user did not select any output device.
+ boolean needBluetoothAudioStart = bluetoothManager.getState() == BluetoothManager.State.HEADSET_AVAILABLE && (userSelectedAudioDevice == AudioDevice.NONE || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
+
+ // Need to stop Bluetooth audio if user selected different device and
+ // Bluetooth SCO connection is established or in the process.
+ boolean needBluetoothAudioStop = (bluetoothManager.getState() == BluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == BluetoothManager.State.SCO_CONNECTING) && (userSelectedAudioDevice != AudioDevice.NONE && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
+
+ if (bluetoothManager.getState() != BluetoothManager.State.HEADSET_AVAILABLE && bluetoothManager.getState() != BluetoothManager.State.SCO_CONNECTING) {
+ bluetoothManager.getState();
+ }
+
+ // Start or stop Bluetooth SCO connection given states set earlier.
+ if (needBluetoothAudioStop) {
+ bluetoothManager.stopScoAudio();
+ bluetoothManager.updateDevice();
+ }
+
+ if (needBluetoothAudioStart && !needBluetoothAudioStop) {
+ // Attempt to start Bluetooth SCO audio (takes a few second to start).
+ if (!bluetoothManager.startScoAudio()) {
+ // Remove BLUETOOTH from list of available devices since SCO failed.
+ audioDevices.remove(AudioDevice.BLUETOOTH);
+ audioDeviceSetUpdated = true;
+ }
+ }
+
+ // Update selected audio device.
+ AudioDevice newAudioDevice;
+
+ if (bluetoothManager.getState() == BluetoothManager.State.SCO_CONNECTED) {
+ // If a Bluetooth is connected, then it should be used as output audio
+ // device. Note that it is not sufficient that a headset is available;
+ // an active SCO channel must also be up and running.
+ newAudioDevice = AudioDevice.BLUETOOTH;
+ } else if (hasWiredHeadset) {
+ // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
+ // audio device.
+ newAudioDevice = AudioDevice.WIRED_HEADSET;
+ } else {
+ // No wired headset and no Bluetooth, hence the audio-device list can contain speaker
+ // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
+ // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
+ // depending on the user's selection.
+ newAudioDevice = defaultAudioDevice;
+ }
+ // Switch to new device but only if there has been any changes.
+ if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
+ // Do the required device switch.
+ setAudioDeviceInternal(newAudioDevice);
+
+ if (audioManagerEvents != null) {
+ // Notify a listening client that audio device has been changed.
+ audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
+ }
+ }
+ }
+
+ /**
+ * AudioDevice is the names of possible audio devices that we currently
+ * support.
+ */
+ public enum AudioDevice {
+ SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
+ }
+
+ /**
+ * AudioManager state.
+ */
+ public enum AudioManagerState {
+ UNINITIALIZED, PREINITIALIZED, RUNNING,
+ }
+
+ /**
+ * Selected audio device change event.
+ */
+ public interface AudioManagerEvents {
+ // Callback fired once audio device is changed or list of available audio devices changed.
+ void onAudioDeviceChanged(AudioDevice selectedAudioDevice, Set availableAudioDevices);
+ }
+
+ /* Receiver which handles changes in wired headset availability. */
+ private class WiredHeadsetReceiver extends BroadcastReceiver {
+ private static final int STATE_UNPLUGGED = 0;
+ private static final int STATE_PLUGGED = 1;
+ private static final int HAS_NO_MIC = 0;
+ private static final int HAS_MIC = 1;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state = intent.getIntExtra("state", STATE_UNPLUGGED);
+ int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
+ String name = intent.getStringExtra("name");
+ hasWiredHeadset = (state == STATE_PLUGGED);
+ updateAudioDeviceState();
+ }
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClient.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClient.kt
new file mode 100644
index 00000000..118951b5
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClient.kt
@@ -0,0 +1,17 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import org.webrtc.IceCandidate
+import org.webrtc.PeerConnection
+import org.webrtc.SessionDescription
+
+interface RTCClient {
+
+ val peerConnection:PeerConnection
+
+ fun onDestroy()
+ fun call()
+ fun answer()
+ fun onRemoteSessionReceived(sessionDescription: SessionDescription)
+ fun addIceCandidateToPeer(iceCandidate: IceCandidate)
+ fun sendIceCandidateToPeer(candidate: IceCandidate, target: String)
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClientImpl.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClientImpl.kt
new file mode 100644
index 00000000..c7413620
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/RTCClientImpl.kt
@@ -0,0 +1,90 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import com.example.studybuddy.domain.model.MessageModel
+import com.example.studybuddy.view.videocall.socket.SocketEvents
+import com.google.gson.Gson
+import org.webrtc.IceCandidate
+import org.webrtc.MediaConstraints
+import org.webrtc.PeerConnection
+import org.webrtc.SessionDescription
+
+class RTCClientImpl(
+ private val connection: PeerConnection,
+ private val username: String,
+ private val target: String,
+ private val gson: Gson,
+ private var listener: WebRTCSignalListener? = null,
+ private val destroyClient: () -> Unit
+) : RTCClient {
+
+ private val mediaConstraint = MediaConstraints().apply {
+ mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
+ mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
+ }
+
+ override val peerConnection = connection
+ override fun call() {
+ peerConnection.createOffer(object : MySdpObserver() {
+ override fun onCreateSuccess(desc: SessionDescription?) {
+ super.onCreateSuccess(desc)
+ peerConnection.setLocalDescription(object : MySdpObserver() {
+ override fun onSetSuccess() {
+ super.onSetSuccess()
+ listener?.onTransferEventToSocket(
+ MessageModel(
+ type = SocketEvents.Offer, name = username, target = target,
+ data = desc?.description
+ )
+ )
+ }
+ }, desc)
+ }
+ }, mediaConstraint)
+ }
+
+ override fun answer() {
+ peerConnection.createAnswer(object : MySdpObserver() {
+ override fun onCreateSuccess(desc: SessionDescription?) {
+ super.onCreateSuccess(desc)
+ peerConnection.setLocalDescription(object : MySdpObserver() {
+ override fun onSetSuccess() {
+ super.onSetSuccess()
+ listener?.onTransferEventToSocket(
+ MessageModel(
+ type = SocketEvents.Answer,
+ name = username,
+ target = target,
+ data = desc?.description
+ )
+ )
+ }
+ }, desc)
+ }
+ }, mediaConstraint)
+ }
+
+ override fun onRemoteSessionReceived(sessionDescription: SessionDescription) {
+ peerConnection.setRemoteDescription(MySdpObserver(), sessionDescription)
+ }
+
+ override fun addIceCandidateToPeer(iceCandidate: IceCandidate) {
+ peerConnection.addIceCandidate(iceCandidate)
+ }
+
+ override fun sendIceCandidateToPeer(candidate: IceCandidate, target: String) {
+ addIceCandidateToPeer(candidate)
+ listener?.onTransferEventToSocket(
+ MessageModel(
+ type = SocketEvents.Ice,
+ name = username,
+ target = target,
+ data = gson.toJson(candidate)
+ )
+ )
+ }
+
+ override fun onDestroy() {
+ connection.close()
+ destroyClient()
+ }
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCFactory.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCFactory.kt
new file mode 100644
index 00000000..5ac567bc
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCFactory.kt
@@ -0,0 +1,181 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import android.content.Context
+import android.util.Log
+import com.example.studybuddy.StudyBuddyApp
+import com.google.gson.Gson
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.webrtc.AudioTrack
+import org.webrtc.Camera2Enumerator
+import org.webrtc.CameraVideoCapturer
+import org.webrtc.DefaultVideoDecoderFactory
+import org.webrtc.DefaultVideoEncoderFactory
+import org.webrtc.EglBase
+import org.webrtc.MediaConstraints
+import org.webrtc.MediaStream
+import org.webrtc.PeerConnection
+import org.webrtc.PeerConnectionFactory
+import org.webrtc.SurfaceTextureHelper
+import org.webrtc.SurfaceViewRenderer
+import org.webrtc.VideoCapturer
+import org.webrtc.VideoTrack
+import javax.inject.Inject
+import javax.inject.Singleton
+
+
+@Singleton
+class WebRTCFactory @Inject constructor(
+ private val context: Context,
+ private val gson: Gson
+) {
+
+ private lateinit var localStreamListener: LocalStreamListener
+ private val eglBaseContext = EglBase.create().eglBaseContext
+ private lateinit var localSurfaceView: SurfaceViewRenderer
+ private lateinit var rtcAudioManager: RTCAudioManager
+
+ private val peerConnectionFactory by lazy { createPeerConnectionFactory() }
+
+ private val iceServer = listOf()
+
+
+ private var screenCapturer: VideoCapturer? = null
+ private val localVideoSource by lazy { peerConnectionFactory.createVideoSource(false) }
+ private val localAudioSource by lazy { peerConnectionFactory.createAudioSource(MediaConstraints()) }
+
+ private val localTrackId = "local_track"
+ private val localStreamId = "${StudyBuddyApp.username}_local_stream_android"
+ private var videoCapturer: CameraVideoCapturer? = null
+ private var localAudioTrack: AudioTrack? = null
+ private var localVideoTrack: VideoTrack? = null
+ private var localStream: MediaStream? = null
+ private val TAG = "WebRTCFactory"
+
+
+ fun init(surface: SurfaceViewRenderer, localStreamListener: LocalStreamListener) {
+ this.localStreamListener = localStreamListener
+ rtcAudioManager = RTCAudioManager.create(context)
+ rtcAudioManager.setDefaultAudioDevice(RTCAudioManager.AudioDevice.SPEAKER_PHONE)
+ rtcAudioManager.start { selectedAudioDevice, availableAudioDevices ->
+ Log.d(
+ TAG,
+ "init: selected $selectedAudioDevice ,available ${availableAudioDevices.toList()}"
+ )
+ }
+ CoroutineScope(Dispatchers.Default).launch {
+
+ initPeerConnectionFactory(context)
+ }
+// this.permissionIntent = permissionIntent
+ initSurfaceView(surface)
+ }
+
+ private fun initSurfaceView(view: SurfaceViewRenderer) {
+ this.localSurfaceView = view
+ view.run {
+ setMirror(false)
+ setEnableHardwareScaler(true)
+ init(eglBaseContext, null)
+ }
+ startLocalVideo(view)
+ }
+
+ fun initRemoteSurfaceView(view: SurfaceViewRenderer) {
+ view.run {
+ setMirror(false)
+ setEnableHardwareScaler(true)
+ init(eglBaseContext, null)
+ }
+ }
+
+ private fun startLocalVideo(surface: SurfaceViewRenderer) {
+ val surfaceTextureHelper =
+ SurfaceTextureHelper.create(Thread.currentThread().name, eglBaseContext)
+ videoCapturer = getVideoCapturer()
+ videoCapturer?.initialize(
+ surfaceTextureHelper,
+ surface.context, localVideoSource.capturerObserver
+ )
+ videoCapturer?.startCapture(480, 320, 10)
+ localVideoTrack =
+ peerConnectionFactory.createVideoTrack(localTrackId + "_video", localVideoSource)
+ localVideoTrack?.addSink(surface)
+ videoCapturer?.switchCamera(null)
+ localAudioTrack =
+ peerConnectionFactory.createAudioTrack(localTrackId + "_audio", localAudioSource)
+ localStream = peerConnectionFactory.createLocalMediaStream(localStreamId)
+ localStream?.addTrack(localAudioTrack)
+ localStream?.addTrack(localVideoTrack)
+ localStreamListener.onLocalStreamReady(localStream!!)
+
+ }
+
+ private fun getVideoCapturer(): CameraVideoCapturer {
+ return Camera2Enumerator(context).run {
+ deviceNames.find {
+ isFrontFacing(it)
+ }?.let {
+ createCapturer(it, null)
+ } ?: throw IllegalStateException()
+ }
+ }
+
+
+ private fun initPeerConnectionFactory(application: Context) {
+ val options = PeerConnectionFactory.InitializationOptions.builder(application)
+ .setEnableInternalTracer(true).setFieldTrials("WebRTC-H264HighProfile/Enabled/")
+ .createInitializationOptions()
+ PeerConnectionFactory.initialize(options)
+ }
+
+ private fun createPeerConnectionFactory(): PeerConnectionFactory {
+ return PeerConnectionFactory.builder().setVideoDecoderFactory(
+ DefaultVideoDecoderFactory(eglBaseContext)
+ ).setVideoEncoderFactory(
+ DefaultVideoEncoderFactory(
+ eglBaseContext, true, true
+ )
+ ).setOptions(PeerConnectionFactory.Options().apply {
+ disableEncryption = false
+ disableNetworkMonitor = false
+ }).createPeerConnectionFactory()
+ }
+
+
+ fun onDestroy() {
+ runCatching {
+ screenCapturer?.stopCapture()
+ screenCapturer?.dispose()
+ localStream?.dispose()
+ }
+ }
+
+
+ fun createRtcClient(
+ observer: PeerConnection.Observer, target: String,
+ listener: WebRTCSignalListener
+ ): RTCClient? {
+ val connection = peerConnectionFactory.createPeerConnection(
+ PeerConnection.RTCConfiguration(iceServer).apply {
+ enableCpuOveruseDetection = true
+ }, observer
+ )
+ connection?.addStream(localStream)
+ return connection?.let {
+ RTCClientImpl(it, StudyBuddyApp.username, target, gson, listener) {
+ destroyProcess()
+ }
+ }
+ }
+
+ private fun destroyProcess() {
+ runCatching {
+ videoCapturer?.stopCapture()
+ videoCapturer?.dispose()
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCSignalListener.kt b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCSignalListener.kt
new file mode 100644
index 00000000..37525c97
--- /dev/null
+++ b/StudyBuddy/app/src/main/java/com/example/studybuddy/view/videocall/webrtc/WebRTCSignalListener.kt
@@ -0,0 +1,9 @@
+package com.example.studybuddy.view.videocall.webrtc
+
+import com.example.studybuddy.domain.model.MessageModel
+
+
+interface WebRTCSignalListener {
+ fun onTransferEventToSocket(data: MessageModel)
+
+}
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/drawable/baseline_add_alarm_24.xml b/StudyBuddy/app/src/main/res/drawable/baseline_add_alarm_24.xml
new file mode 100644
index 00000000..310f70a0
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/baseline_add_alarm_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/baseline_call_24.xml b/StudyBuddy/app/src/main/res/drawable/baseline_call_24.xml
new file mode 100644
index 00000000..fbd2b0a2
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/baseline_call_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/baseline_check_24.xml b/StudyBuddy/app/src/main/res/drawable/baseline_check_24.xml
new file mode 100644
index 00000000..562b6210
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/baseline_check_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/baseline_visibility_24.xml b/StudyBuddy/app/src/main/res/drawable/baseline_visibility_24.xml
new file mode 100644
index 00000000..f843e291
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/baseline_visibility_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/baseline_visibility_off_24.xml b/StudyBuddy/app/src/main/res/drawable/baseline_visibility_off_24.xml
new file mode 100644
index 00000000..5993ca39
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/baseline_visibility_off_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/books.png b/StudyBuddy/app/src/main/res/drawable/books.png
new file mode 100644
index 00000000..a4215e65
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/books.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/boyimg.png b/StudyBuddy/app/src/main/res/drawable/boyimg.png
new file mode 100644
index 00000000..c839df4e
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/boyimg.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/chatfilled.xml b/StudyBuddy/app/src/main/res/drawable/chatfilled.xml
new file mode 100644
index 00000000..7f6fda16
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/chatfilled.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/chatoutlined.xml b/StudyBuddy/app/src/main/res/drawable/chatoutlined.xml
new file mode 100644
index 00000000..7ce81fa5
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/chatoutlined.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/focus.png b/StudyBuddy/app/src/main/res/drawable/focus.png
new file mode 100644
index 00000000..73eff55f
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/focus.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/focusfill.png b/StudyBuddy/app/src/main/res/drawable/focusfill.png
new file mode 100644
index 00000000..ef6e4480
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/focusfill.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/homefilled.xml b/StudyBuddy/app/src/main/res/drawable/homefilled.xml
new file mode 100644
index 00000000..20cb4d6c
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/homefilled.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/homeoutlined.xml b/StudyBuddy/app/src/main/res/drawable/homeoutlined.xml
new file mode 100644
index 00000000..4d8dd31c
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/homeoutlined.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/ic_launcher_background.xml b/StudyBuddy/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/ic_launcher_foreground.xml b/StudyBuddy/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/drawable/instagram.png b/StudyBuddy/app/src/main/res/drawable/instagram.png
new file mode 100644
index 00000000..9b403cfc
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/instagram.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/lamp.jpg b/StudyBuddy/app/src/main/res/drawable/lamp.jpg
new file mode 100644
index 00000000..7947994f
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/lamp.jpg differ
diff --git a/StudyBuddy/app/src/main/res/drawable/login.png b/StudyBuddy/app/src/main/res/drawable/login.png
new file mode 100644
index 00000000..bf2159ba
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/login.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/logincomplete.png b/StudyBuddy/app/src/main/res/drawable/logincomplete.png
new file mode 100644
index 00000000..ad8aa41d
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/logincomplete.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/logo.jpg b/StudyBuddy/app/src/main/res/drawable/logo.jpg
new file mode 100644
index 00000000..084740e5
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/logo.jpg differ
diff --git a/StudyBuddy/app/src/main/res/drawable/questionfill.xml b/StudyBuddy/app/src/main/res/drawable/questionfill.xml
new file mode 100644
index 00000000..10b1312e
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/questionfill.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/questionoutline.xml b/StudyBuddy/app/src/main/res/drawable/questionoutline.xml
new file mode 100644
index 00000000..d494015a
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/questionoutline.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/signup.png b/StudyBuddy/app/src/main/res/drawable/signup.png
new file mode 100644
index 00000000..33796708
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/signup.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/splash_screen.png b/StudyBuddy/app/src/main/res/drawable/splash_screen.png
new file mode 100644
index 00000000..730892cf
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/splash_screen.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/todo.jpg b/StudyBuddy/app/src/main/res/drawable/todo.jpg
new file mode 100644
index 00000000..9322d71d
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/todo.jpg differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user.png b/StudyBuddy/app/src/main/res/drawable/user.png
new file mode 100644
index 00000000..10e1f2a3
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user1.png b/StudyBuddy/app/src/main/res/drawable/user1.png
new file mode 100644
index 00000000..2c71e6c5
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user1.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user2.png b/StudyBuddy/app/src/main/res/drawable/user2.png
new file mode 100644
index 00000000..897a9f30
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user2.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user3.png b/StudyBuddy/app/src/main/res/drawable/user3.png
new file mode 100644
index 00000000..e064e66b
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user3.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user4.png b/StudyBuddy/app/src/main/res/drawable/user4.png
new file mode 100644
index 00000000..086adc78
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user4.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user5.png b/StudyBuddy/app/src/main/res/drawable/user5.png
new file mode 100644
index 00000000..d2aaefab
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user5.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user6.png b/StudyBuddy/app/src/main/res/drawable/user6.png
new file mode 100644
index 00000000..1db461c6
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user6.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user7.png b/StudyBuddy/app/src/main/res/drawable/user7.png
new file mode 100644
index 00000000..288bbc98
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user7.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/user8.png b/StudyBuddy/app/src/main/res/drawable/user8.png
new file mode 100644
index 00000000..40f4bd9e
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/user8.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/videocall.xml b/StudyBuddy/app/src/main/res/drawable/videocall.xml
new file mode 100644
index 00000000..908ef0c9
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/videocall.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/videocallfill.xml b/StudyBuddy/app/src/main/res/drawable/videocallfill.xml
new file mode 100644
index 00000000..fe48ccca
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/videocallfill.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/videocalloutline.xml b/StudyBuddy/app/src/main/res/drawable/videocalloutline.xml
new file mode 100644
index 00000000..dfcca18a
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/drawable/videocalloutline.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/StudyBuddy/app/src/main/res/drawable/whatsapp.png b/StudyBuddy/app/src/main/res/drawable/whatsapp.png
new file mode 100644
index 00000000..2e7d5fbf
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/whatsapp.png differ
diff --git a/StudyBuddy/app/src/main/res/drawable/youtube.png b/StudyBuddy/app/src/main/res/drawable/youtube.png
new file mode 100644
index 00000000..2678f09d
Binary files /dev/null and b/StudyBuddy/app/src/main/res/drawable/youtube.png differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/StudyBuddy/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/StudyBuddy/app/src/main/res/values/arrays.xml b/StudyBuddy/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..141bfc01
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/values/arrays.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/values/colors.xml b/StudyBuddy/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..f8c6127d
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/values/strings.xml b/StudyBuddy/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ba2db659
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Study Buddy
+ Service to block certain apps
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/values/themes.xml b/StudyBuddy/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..90096a85
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/xml/accessibility_service_config.xml b/StudyBuddy/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 00000000..4689e818
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/StudyBuddy/app/src/main/res/xml/backup_rules.xml b/StudyBuddy/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..fa0f996d
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/main/res/xml/data_extraction_rules.xml b/StudyBuddy/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..9ee9997b
--- /dev/null
+++ b/StudyBuddy/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StudyBuddy/app/src/test/java/com/example/studybuddy/ExampleUnitTest.kt b/StudyBuddy/app/src/test/java/com/example/studybuddy/ExampleUnitTest.kt
new file mode 100644
index 00000000..0d4d3b74
--- /dev/null
+++ b/StudyBuddy/app/src/test/java/com/example/studybuddy/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.studybuddy
+
+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
diff --git a/StudyBuddy/build.gradle.kts b/StudyBuddy/build.gradle.kts
new file mode 100644
index 00000000..1dfc60e2
--- /dev/null
+++ b/StudyBuddy/build.gradle.kts
@@ -0,0 +1,8 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+ id("com.google.dagger.hilt.android") version "2.48" apply false
+ id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
+ alias(libs.plugins.google.gms.google.services) apply false
+}
\ No newline at end of file
diff --git a/StudyBuddy/gradle.properties b/StudyBuddy/gradle.properties
new file mode 100644
index 00000000..20e2a015
--- /dev/null
+++ b/StudyBuddy/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/StudyBuddy/gradle/libs.versions.toml b/StudyBuddy/gradle/libs.versions.toml
new file mode 100644
index 00000000..d04f462c
--- /dev/null
+++ b/StudyBuddy/gradle/libs.versions.toml
@@ -0,0 +1,66 @@
+[versions]
+agp = "8.4.0-rc02"
+composeBomVersion = "2024.06.00"
+destinationVersion = "1.10.0"
+desugar_jdk_libs = "2.0.4"
+hiltAndroid = "2.48"
+hiltAndroidCompiler = "2.48"
+hiltNavigationCompose = "1.2.0"
+kotlin = "1.9.0"
+coreKtx = "1.13.1"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+lifecycleRuntimeCompose = "2.8.3"
+lifecycleRuntimeKtx = "2.8.2"
+activityCompose = "1.9.0"
+composeBom = "2023.08.00"
+roomVersion = "2.6.1"
+uiTextGoogleFonts = "1.6.8"
+googleGmsGoogleServices = "4.4.2"
+firebaseAuth = "23.0.0"
+runtimeLivedata = "1.6.8"
+appcompat = "1.7.0"
+material = "1.12.0"
+activity = "1.9.2"
+
+[libraries]
+androidx-compose-bom-v20240600 = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" }
+androidx-compose-material-icons-extended-android = { module = "androidx.compose.material:compose-material-icons-extended-android" }
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigationCompose" }
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
+androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" }
+compose-destinations-ksp = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "destinationVersion" }
+core = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "destinationVersion" }
+dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
+desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
+hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+firebase-auth = { group = "com.google.firebase", name = "firebase-auth", version.ref = "firebaseAuth" }
+androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" }
+
diff --git a/StudyBuddy/gradle/wrapper/gradle-wrapper.jar b/StudyBuddy/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..e708b1c0
Binary files /dev/null and b/StudyBuddy/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/StudyBuddy/gradle/wrapper/gradle-wrapper.properties b/StudyBuddy/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3be2b499
--- /dev/null
+++ b/StudyBuddy/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jul 05 19:53:52 IST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/StudyBuddy/gradlew b/StudyBuddy/gradlew
new file mode 100644
index 00000000..4f906e0c
--- /dev/null
+++ b/StudyBuddy/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/StudyBuddy/gradlew.bat b/StudyBuddy/gradlew.bat
new file mode 100644
index 00000000..107acd32
--- /dev/null
+++ b/StudyBuddy/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/StudyBuddy/settings.gradle.kts b/StudyBuddy/settings.gradle.kts
new file mode 100644
index 00000000..c8a8828a
--- /dev/null
+++ b/StudyBuddy/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Study Buddy"
+include(":app")
+
\ No newline at end of file