diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index b93d6fb..acb3915 100755
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,7 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
+title: ''
labels: bug
+assignees: ''
+
---
**Describe the bug**
@@ -9,23 +12,20 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
-1.
-2.
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
-**Expected behavior**
+**Expected behaviour**
A clear and concise description of what you expected to happen.
-**Environment**
-A concise description of your environment: GatorGrader version,
-python version, operating system, etc.
-
-**Possible solution**
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
-**Possible implementation**
+**Smartphone (please complete the following information):**
+ - Device: [e.g. Pixel9]
+ - OS: [e.g. Android 16]
**Additional context**
Add any other context about the problem here.
-
-#### Is this is a critical secuirty vulnerability?
-- [ ] Yes: Please read code-of-conduct once
-- [ ] No
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100755
index b09ad19..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-labels: feature
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
-
-#### Will you be working on this feature?
-- [ ] Yes
-- [ ] No
-
-#### Have you talked with any inner-core member regarding this feature?
-- [ ] Yes
-- [ ] No
diff --git a/.gitignore b/.gitignore
index 7e5b563..8715c49 100755
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,6 @@
.externalNativeBuild
.cxx
/app/google-services.json
+/app/google-services.prod.json
+/app/google-services.dev.json
/keystore.properties
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..0325727
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+VITTY
\ No newline at end of file
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8..b86273d 100755
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/detekt.xml b/.idea/detekt.xml
new file mode 100644
index 0000000..ee7289c
--- /dev/null
+++ b/.idea/detekt.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 7b46144..804c374 100755
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,14 +4,14 @@
-
-
+
-
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7061a0d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..fe63bb6
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml
new file mode 100644
index 0000000..e8bd90c
--- /dev/null
+++ b/.idea/ktlint-plugin.xml
@@ -0,0 +1,7 @@
+
+
+
+ DISTRACT_FREE
+ DEFAULT
+
+
\ No newline at end of file
diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000..f286756
--- /dev/null
+++ b/.idea/material_theme_project_new.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 6d6971a..07a0a33 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
@@ -67,7 +66,8 @@
-
+
+
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..35eb1dd 100755
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 4f77557..c4186b6 100755
--- a/README.md
+++ b/README.md
@@ -3,35 +3,46 @@
VITTY - VIT Timetable App
- An Android app for your VIT timetable with homescreen widgets and in-app notifications for your classes
+ An Android app for your VIT timetable with homescreen widgets, caching, friend & circle features, ghost mode, reminders, and in-app notifications for your classes
---
+
[](https://dsc.community.dev/vellore-institute-of-technology/)
[](https://discord.gg/498KVdSKWR)
-[](https://github.com/GDGVIT/vitty-app/blob/master/README.md)
- [](https://www.figma.com/file/3ILW1qy1qIjiJ5S78zyIqh/VITTY?node-id=1%3A4)
-
+[](https://github.com/GDGVIT/vitty-app/blob/master/README.md)
+[](https://www.figma.com/file/3ILW1qy1qIjiJ5S78zyIqh/VITTY?node-id=1%3A4)
## Features
-- [x] Easy access to timetable
-- [x] Home screen widgets
-- [x] Notifications for classes
-- [x] Exam/Holiday mode to turn off your class notifications
-- [x] Navigation directions to your classes
+
+- [x] Easy access to your VIT timetable (cached & offline-ready)
+- [x] Home screen widgets (Today, Next Class)
+- [x] Notifications before classes with Exam/Holiday mode to disable them
+- [x] Navigation directions to class venues
+- [x] Friend system (send requests, view schedules)
+- [x] Ghost Mode – hide your timetable from friends
+- [x] Circles – group participants, view shared schedules
+- [x] Reminders – attach, edit, or delete course reminders
+- [x] Notes – markdown editor with undo/redo and local Room database
+- [x] QR code scan & generate for joining circles
+- [x] Community integration with rich friend profiles and actions
+- [x] Support & Feedback section in Settings
## Dependencies
- - Android SDK
- - Android Studio
+- Android SDK
+- Android Studio (latest stable)
+- Kotlin, Jetpack Compose
+- Room, LiveData, ViewModel
## Running
-- Import the project in Android Studio
-- Run the project using the automatically added APP configuration
+- Clone the repository
+- Open it in Android Studio
+- Sync Gradle and run the app using the default `APP` configuration
## Contributors
@@ -51,9 +62,37 @@
+
+ Rudrank Basant
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jothish Kamal
+
+
+
+
+
+
+
+
+
+
+
+
- Made with :heart: by GDSC VIT
+ Made with ❤️ by GDSC VIT
diff --git a/app/build.gradle b/app/build.gradle
index 7a5acc9..20d3f6d 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,33 +2,41 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs.kotlin'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
- id 'org.jlleitschuh.gradle.ktlint' version '10.0.0'
+ id 'org.jlleitschuh.gradle.ktlint' version '13.0.0'
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.2.0'
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
-keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+} else {
+ throw new GradleException("Missing keystore.properties file!")
+}
android {
+ compileSdk 36
+
signingConfigs {
release {
- keyAlias keystoreProperties['keyAlias']
- keyPassword keystoreProperties['keyPassword']
- storeFile file(keystoreProperties['storeFile'])
- storePassword keystoreProperties['storePassword']
+ keyAlias keystoreProperties["keyAlias"]
+ keyPassword keystoreProperties["keyPassword"]
+ storeFile file(keystoreProperties["storeFile"])
+ storePassword keystoreProperties["storePassword"]
}
}
- compileSdkVersion 33
- buildToolsVersion "30.0.3"
+ // buildToolsVersion "30.0.3"
defaultConfig {
- applicationId "com.dscvit.vitty"
- minSdkVersion 24
- targetSdkVersion 33
- versionCode 34
- versionName "2.0.0"
+ namespace "com.dscvit.vitty"
+ minSdkVersion 26
+ targetSdkVersion 36
+ versionCode 44
+ versionName "3.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
signingConfig signingConfigs.release
}
@@ -39,67 +47,110 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
+ kotlin{
+ jvmToolchain(21)
}
buildFeatures {
+ viewBinding true
dataBinding true
+ buildConfig true
+ compose true
}
}
dependencies {
+ // Compose BOM (Bill of Materials)
+ implementation platform("androidx.compose:compose-bom:2025.07.00")
+
+ // Core Compose dependencies
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.material:material"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation "androidx.activity:activity-compose"
+
+ // For interoperability
+ implementation "androidx.compose.ui:ui-viewbinding"
// Android Stuff
- implementation 'androidx.core:core-ktx:1.8.0'
- implementation 'androidx.appcompat:appcompat:1.4.2'
- implementation 'com.google.android.material:material:1.6.1'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.core:core-ktx:1.16.0'
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
- implementation 'androidx.preference:preference-ktx:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.2'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.2'
+ implementation 'androidx.navigation:navigation-compose:2.9.2'
+ implementation 'androidx.compose.material3:material3-android:1.3.2'
+
+ implementation 'androidx.compose.runtime:runtime-livedata:1.8.3'
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
// Firebase
- implementation platform('com.google.firebase:firebase-bom:27.1.0')
+ implementation platform('com.google.firebase:firebase-bom:33.16.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
// Firebase Auth
implementation 'com.google.firebase:firebase-auth-ktx'
- implementation 'com.google.android.gms:play-services-auth:19.2.0'
+ implementation 'com.google.android.gms:play-services-auth:21.3.0'
- // Firestore
- implementation 'com.google.firebase:firebase-firestore:24.2.0'
+ // FireStore
+ implementation 'com.google.firebase:firebase-firestore:25.1.4'
// FCM
- implementation 'com.google.firebase:firebase-messaging:23.0.6'
+ implementation 'com.google.firebase:firebase-messaging:24.1.2'
// Remote Config
implementation 'com.google.firebase:firebase-config-ktx'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.8.3'
// Crashlytics
releaseImplementation 'com.google.firebase:firebase-crashlytics-ktx'
// Retrofit
- implementation 'com.squareup.retrofit2:retrofit:2.5.0'
- implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
- implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
+ implementation 'com.squareup.retrofit2:retrofit:3.0.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
+ implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
// Loading Animations
implementation 'com.github.ybq:Android-SpinKit:1.4.0'
// Coil
- implementation "io.coil-kt:coil:1.4.0"
-
+ implementation "io.coil-kt:coil:2.7.0"
+ implementation("io.coil-kt:coil-compose:2.7.0")
// Timber for logs
- implementation 'com.jakewharton.timber:timber:4.7.1'
+ implementation 'com.jakewharton.timber:timber:5.0.1'
+
+ // Room Database
+ implementation "androidx.room:room-runtime:2.7.2"
+ implementation "androidx.room:room-ktx:2.7.2"
+ kapt "androidx.room:room-compiler:2.7.2"
+
+ // ViewModel and LiveData
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2"
+ implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.2"
+
+ implementation 'androidx.dynamicanimation:dynamicanimation-ktx:1.1.0'
+
+ implementation project(':markdowntext')
+
+ // QR Code generation
+ implementation 'com.google.zxing:core:3.5.3'
+ implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
+
+ // Camera permissions for QR scanning
+ implementation 'com.google.accompanist:accompanist-permissions:0.37.3'
+
+ // Google Play In-App Updates
+ implementation 'com.google.android.play:app-update:2.1.0'
+ implementation 'com.google.android.play:app-update-ktx:2.1.0'
+
+ // Google Play In-App Review
+ implementation "com.google.android.play:review:2.0.2"
+ implementation "com.google.android.play:review-ktx:2.0.2"
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a01a9fc..9f8938b 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,11 +1,18 @@
-
+
+
+
+
+
+
+
@@ -23,10 +30,17 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.VITTY">
+ android:theme="@style/Theme.VITTY"
+ android:usesCleartextTraffic="true">
+
+
@@ -91,6 +105,7 @@
+
+
=
hashMapOf(
- LATEST_VERSION to BuildConfig.VERSION_CODE,
+ LATEST_VERSION to BuildConfig.VERSION_NAME,
ONLINE_MODE to false
)
diff --git a/app/src/main/java/com/dscvit/vitty/activity/AddInfoActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/AddInfoActivity.kt
new file mode 100644
index 0000000..2cffe56
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/activity/AddInfoActivity.kt
@@ -0,0 +1,343 @@
+package com.dscvit.vitty.activity
+
+import android.app.Activity
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Bundle
+import android.os.PowerManager
+import android.provider.Settings
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import com.dscvit.vitty.BuildConfig
+import com.dscvit.vitty.R
+import com.dscvit.vitty.databinding.ActivityAddInfoBinding
+import com.dscvit.vitty.receiver.AlarmReceiver
+import com.dscvit.vitty.ui.auth.AuthViewModel
+import com.dscvit.vitty.util.ArraySaverLoader
+import com.dscvit.vitty.util.Constants
+import com.dscvit.vitty.util.LogoutHelper
+import com.dscvit.vitty.util.NotificationHelper
+import com.dscvit.vitty.util.UtilFunctions
+import com.google.firebase.firestore.FirebaseFirestore
+import com.google.firebase.firestore.Source
+import timber.log.Timber
+import java.util.Date
+
+class AddInfoActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityAddInfoBinding
+ private lateinit var authViewModel: AuthViewModel
+ private lateinit var prefs: SharedPreferences
+ private val db = FirebaseFirestore.getInstance()
+ private val days =
+ listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
+ private var uid = ""
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_add_info)
+ authViewModel = ViewModelProvider(this)[AuthViewModel::class.java]
+ prefs = getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE)
+ uid = prefs.getString(Constants.UID, "").toString()
+ setupToolbar()
+ setGDSCVITChannel()
+ setupCampusSpinner()
+
+ binding.continueButton.setOnClickListener {
+ setupContinueButton()
+ }
+
+ authViewModel.signInResponse.observe(this) {
+ if (it != null) {
+ prefs.edit().putString(Constants.COMMUNITY_TOKEN, it.token).apply()
+ prefs.edit().putString(Constants.COMMUNITY_NAME, it.name).apply()
+ prefs.edit().putString(Constants.COMMUNITY_PICTURE, it.picture).apply()
+ } else {
+ Toast.makeText(this, R.string.something_went_wrong, Toast.LENGTH_LONG).show()
+ binding.loadingView.visibility = View.GONE
+ }
+ }
+
+ authViewModel.user.observe(this) {
+ Timber.d("User: $it")
+ if (it != null) {
+ val timetableDays = it.timetable?.data
+ if (!timetableDays?.Monday.isNullOrEmpty() ||
+ !timetableDays?.Tuesday.isNullOrEmpty() ||
+ !timetableDays?.Wednesday.isNullOrEmpty() ||
+ !timetableDays?.Thursday.isNullOrEmpty() ||
+ !timetableDays?.Friday.isNullOrEmpty() ||
+ !timetableDays?.Saturday.isNullOrEmpty() ||
+ !timetableDays?.Sunday.isNullOrEmpty()
+ ) {
+ binding.loadingView.visibility = View.GONE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannels()
+ } else {
+ tellUpdated()
+ }
+ prefs.edit().putBoolean(Constants.COMMUNITY_TIMETABLE_AVAILABLE, true).apply()
+ } else {
+ val intent = Intent(this, InstructionsActivity::class.java)
+ binding.loadingView.visibility = View.GONE
+ startActivity(intent)
+ finish()
+ }
+ } else {
+ Toast.makeText(this, R.string.something_went_wrong, Toast.LENGTH_LONG).show()
+ binding.loadingView.visibility = View.GONE
+ }
+ }
+
+ binding.etUsername.addTextChangedListener(
+ object : TextWatcher {
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int,
+ ) {}
+
+ override fun onTextChanged(
+ s: CharSequence?,
+ start: Int,
+ before: Int,
+ count: Int,
+ ) {
+ val username = s.toString().trim()
+ authViewModel.checkUsername(username)
+ }
+
+ override fun afterTextChanged(s: Editable?) {
+ }
+ },
+ )
+
+ authViewModel.usernameValidity.observe(this) {
+ Timber.d("Validity: $it")
+ if (it != null) {
+ binding.usernameValidity.visibility = View.VISIBLE
+ binding.usernameValidity.text = it.detail
+ if (it.detail == "Username is valid") {
+ binding.usernameValidity.setTextColor(getColor(R.color.white))
+ } else {
+ binding.usernameValidity.setTextColor(getColor(R.color.red))
+ }
+ }
+ }
+ }
+
+ private fun setupCampusSpinner() {
+ val campusOptions = arrayOf("Select Campus", "Vellore", "Chennai", "Bhopal")
+ val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, campusOptions)
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ binding.spinnerCampus.adapter = adapter
+ }
+
+ private fun setupContinueButton() {
+ binding.loadingView.visibility = View.VISIBLE
+ val uuid = prefs.getString(Constants.UID, null)
+ val username =
+ binding.etUsername.text
+ .toString()
+ .trim()
+ val regno =
+ binding.etRegno.text
+ .toString()
+ .uppercase()
+ .trim()
+ val selectedCampus = binding.spinnerCampus.selectedItem.toString()
+ val regexPattern = Regex("^[0-9]{2}[a-zA-Z]{3}[0-9]{4}$")
+
+ if (username.isEmpty() || regno.isEmpty() || selectedCampus == "Select Campus") {
+ Toast.makeText(this, getString(R.string.fill_all_fields), Toast.LENGTH_LONG).show()
+ binding.loadingView.visibility = View.GONE
+ } else if (!regexPattern.matches(regno)) {
+ Toast.makeText(this, getString(R.string.invalid_regno), Toast.LENGTH_LONG).show()
+ binding.loadingView.visibility = View.GONE
+ } else {
+ if (uuid != null) {
+ prefs.edit().putString(Constants.COMMUNITY_USERNAME, username).apply()
+ prefs.edit { putString(Constants.COMMUNITY_REGNO, regno) }
+ prefs.edit { putString(Constants.COMMUNITY_CAMPUS, selectedCampus) }
+ authViewModel.signInAndGetTimeTable(username, regno, uuid, selectedCampus.lowercase())
+ } else {
+ Toast
+ .makeText(this, getString(R.string.something_went_wrong), Toast.LENGTH_LONG)
+ .show()
+ binding.loadingView.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun setupToolbar() {
+ binding.addInfoToolbar.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ R.id.close -> {
+ LogoutHelper.logout(this, this as Activity, prefs)
+ true
+ }
+
+ else -> false
+ }
+ }
+ }
+
+ private fun createNotificationChannels() {
+ binding.loadingView.visibility = View.VISIBLE
+ setNotificationGroup()
+ val notifChannels = ArraySaverLoader.loadArray(Constants.NOTIFICATION_CHANNELS, this)
+ for (notifChannel in notifChannels) {
+ if (notifChannel != null) {
+ NotificationHelper.deleteNotificationChannel(this, notifChannel.toString())
+ }
+ }
+ val newNotifChannels: ArrayList = ArrayList()
+ for (day in days) {
+ db
+ .collection("users")
+ .document(uid)
+ .collection("timetable")
+ .document(day)
+ .collection("periods")
+ .get(Source.SERVER)
+ .addOnSuccessListener { result ->
+ for (document in result) {
+ var cn = document.getString("courseName")
+ cn = if (cn.isNullOrEmpty()) "Default" else cn
+ val cc = document.getString("courseCode")
+ NotificationHelper.createNotificationChannel(
+ this,
+ cn,
+ "Course Code: $cc",
+ Constants.GROUP_ID,
+ )
+ newNotifChannels.add(cn)
+ Timber.d(cn)
+ }
+ ArraySaverLoader.saveArray(
+ newNotifChannels,
+ Constants.NOTIFICATION_CHANNELS,
+ this,
+ )
+
+ if (day == "sunday") {
+ tellUpdated()
+ }
+ }.addOnFailureListener { e ->
+ Timber.d("Error: $e")
+ }
+ }
+ }
+
+ private fun tellUpdated() {
+ prefs.edit().putInt(Constants.TIMETABLE_AVAILABLE, 1).apply()
+ prefs.edit().putInt(Constants.UPDATE, 0).apply()
+ val updated =
+ hashMapOf(
+ "isTimetableAvailable" to true,
+ "isUpdated" to false,
+ )
+ db
+ .collection("users")
+ .document(uid)
+ .set(updated)
+ .addOnSuccessListener {
+ setAlarm()
+ UtilFunctions.reloadWidgets(this)
+ val pm: PowerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (!pm.isIgnoringBatteryOptimizations(packageName)) {
+ Toast
+ .makeText(
+ this,
+ "Please turn off the Battery Optimization Settings for VITTY to receive notifications on time.",
+ Toast.LENGTH_LONG,
+ ).show()
+ val pmIntent = Intent()
+ pmIntent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
+ startActivity(pmIntent)
+ } else {
+ val intent = Intent(this, HomeComposeActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
+ }.addOnFailureListener { e ->
+ Timber.d("Error: $e")
+ }
+ }
+
+ private fun setGDSCVITChannel() {
+ if (!prefs.getBoolean("gdscvitChannelCreated", false)) {
+ NotificationHelper.createNotificationGroup(
+ this,
+ getString(R.string.gdscvit),
+ Constants.GROUP_ID_2,
+ )
+ NotificationHelper.createNotificationChannel(
+ this,
+ getString(R.string.default_notification_channel_name),
+ "Notifications from GDSC VIT",
+ Constants.GROUP_ID_2,
+ )
+ prefs.edit {
+ putBoolean("gdscvitChannelCreated", true)
+ apply()
+ }
+ }
+ }
+
+ private fun setNotificationGroup() {
+ if (!prefs.getBoolean("groupCreated", false)) {
+ NotificationHelper.createNotificationGroup(
+ this,
+ getString(R.string.notif_group),
+ Constants.GROUP_ID,
+ )
+ prefs.edit {
+ putBoolean("groupCreated", true)
+ apply()
+ }
+ }
+ }
+
+ private fun setAlarm() {
+ if (!prefs.getBoolean(Constants.EXAM_MODE, false)) {
+ if (prefs.getInt(Constants.VERSION_CODE, 0) != BuildConfig.VERSION_CODE) {
+ val intent = Intent(this, AlarmReceiver::class.java)
+
+ val pendingIntent =
+ PendingIntent.getBroadcast(
+ this,
+ Constants.ALARM_INTENT,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
+
+ val date = Date().time
+
+ alarmManager.setRepeating(
+ AlarmManager.RTC_WAKEUP,
+ date,
+ (1000 * 60 * Constants.NOTIF_DELAY).toLong(),
+ pendingIntent,
+ )
+
+ prefs.edit {
+ putInt(Constants.VERSION_CODE, BuildConfig.VERSION_CODE)
+ apply()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt
index c984561..a58db1c 100755
--- a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt
+++ b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt
@@ -1,20 +1,31 @@
package com.dscvit.vitty.activity
+import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
import androidx.viewpager2.widget.ViewPager2
import com.dscvit.vitty.R
import com.dscvit.vitty.adapter.IntroAdapter
import com.dscvit.vitty.databinding.ActivityAuthBinding
+import com.dscvit.vitty.ui.auth.AuthViewModel
+import com.dscvit.vitty.util.Constants
import com.dscvit.vitty.util.Constants.TOKEN
import com.dscvit.vitty.util.Constants.UID
import com.dscvit.vitty.util.Constants.USER_INFO
+import com.dscvit.vitty.util.NotificationPermissionHelper
+import com.dscvit.vitty.util.MaintenanceChecker
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
@@ -24,9 +35,9 @@ import com.google.android.gms.tasks.Task
import com.google.android.material.tabs.TabLayoutMediator
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.GoogleAuthProvider
+import timber.log.Timber
class AuthActivity : AppCompatActivity() {
-
private lateinit var binding: ActivityAuthBinding
private val SIGNIN: Int = 1
@@ -34,34 +45,130 @@ class AuthActivity : AppCompatActivity() {
private lateinit var mGoogleSignInOptions: GoogleSignInOptions
private lateinit var firebaseAuth: FirebaseAuth
private lateinit var sharedPref: SharedPreferences
+ private lateinit var authViewModel: AuthViewModel
private val pages = listOf("○", "○", "○")
private var loginClick = false
+ private lateinit var notificationPermissionLauncher: ActivityResultLauncher
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_auth)
firebaseAuth = FirebaseAuth.getInstance()
sharedPref = getSharedPreferences(USER_INFO, Context.MODE_PRIVATE)
+ authViewModel = ViewModelProvider(this)[AuthViewModel::class.java]
+
+ setupNotificationPermissionLauncher()
+ requestNotificationPermissionIfNeeded()
+
configureGoogleSignIn()
setupUI()
+ setupBackPressedHandler()
+ }
+
+ private fun setupBackPressedHandler() {
+ val callback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ binding.apply {
+ if (introPager.currentItem == 0 || loginClick) {
+ finish()
+ } else {
+ introPager.currentItem--
+ }
+ }
+ }
+ }
+ onBackPressedDispatcher.addCallback(this, callback)
+ }
+
+ private fun setupNotificationPermissionLauncher() {
+ notificationPermissionLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.RequestPermission(),
+ ) { isGranted ->
+ if (isGranted) {
+ Timber.d("Notification permission granted")
+ // Check and request exact alarm permission if needed
+ if (!NotificationPermissionHelper.canScheduleExactAlarms(this)) {
+ NotificationPermissionHelper.requestExactAlarmPermission(this)
+ }
+ } else {
+ Timber.d("Notification permission denied")
+ Toast
+ .makeText(
+ this,
+ "Notification permission is required for reminders to work properly",
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+ }
+ }
+
+ private fun requestNotificationPermissionIfNeeded() {
+ // For Android 13+ (API 33+), request POST_NOTIFICATIONS permission
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (!NotificationPermissionHelper.hasNotificationPermission(this)) {
+ Timber.d("Requesting POST_NOTIFICATIONS permission for Android 13+")
+ notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ Timber.d("POST_NOTIFICATIONS permission already granted")
+ checkAdditionalPermissions()
+ }
+ } else {
+ // For Android 12 and below, notifications are enabled by default
+ // but we still need to check other permissions
+ Timber.d("Android version < 13, notifications enabled by default")
+ checkAdditionalPermissions()
+ }
+ }
+
+ private fun checkAdditionalPermissions() {
+ // Check and request exact alarm permission if needed (Android 12+)
+ if (!NotificationPermissionHelper.canScheduleExactAlarms(this)) {
+ Timber.d("Requesting exact alarm permission")
+ NotificationPermissionHelper.requestExactAlarmPermission(this)
+ }
+
+ // Note: Battery optimization can be checked later in the app flow
+ // as it's more intrusive and not immediately necessary
}
override fun onStart() {
super.onStart()
+ sharedPref = getSharedPreferences(USER_INFO, Context.MODE_PRIVATE)
+ val isTimeTableAvailable =
+ sharedPref.getBoolean(Constants.COMMUNITY_TIMETABLE_AVAILABLE, false)
+ val token = sharedPref.getString(Constants.COMMUNITY_TOKEN, null)
+ val username = sharedPref.getString(Constants.COMMUNITY_USERNAME, null)
+ val regno = sharedPref.getString(Constants.COMMUNITY_REGNO, null)
val user = FirebaseAuth.getInstance().currentUser
- if (user != null) {
+ Timber.d("isTimeTableAvailable: $isTimeTableAvailable token: $token username: $username regno: $regno")
+ if (isTimeTableAvailable) {
+ val intent = Intent(this, HomeComposeActivity::class.java)
+ startActivity(intent)
+ finish()
+ } else if (token != null && username != null) {
val intent = Intent(this, InstructionsActivity::class.java)
startActivity(intent)
finish()
+ } else {
+ Timber.d("here going to add info")
+ if (user != null) {
+ val intent = Intent(this, AddInfoActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
}
}
private fun configureGoogleSignIn() {
- mGoogleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
- .requestIdToken(getString(R.string.default_web_client_id))
- .requestEmail()
- .build()
+ mGoogleSignInOptions =
+ GoogleSignInOptions
+ .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestIdToken(getString(R.string.default_web_client_id))
+ .requestEmail()
+ .build()
mGoogleSignInClient = GoogleSignIn.getClient(this, mGoogleSignInOptions)
mGoogleSignInClient.signOut()
}
@@ -70,30 +177,33 @@ class AuthActivity : AppCompatActivity() {
val pagerAdapter = IntroAdapter(this)
binding.introPager.adapter = pagerAdapter
- val pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
- override fun onPageSelected(position: Int) {
- binding.apply {
- if (position == 0 || position == 1) {
- loginButton.visibility = View.INVISIBLE
- nextButton.visibility = View.VISIBLE
- } else {
- nextButton.visibility = View.INVISIBLE
- loginButton.visibility = View.VISIBLE
+ val pageChangeCallback =
+ object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ binding.apply {
+ if (position == 0 || position == 1) {
+ loginButton.visibility = View.INVISIBLE
+ nextButton.visibility = View.VISIBLE
+ } else {
+ nextButton.visibility = View.INVISIBLE
+ loginButton.visibility = View.VISIBLE
+ }
}
}
}
- }
binding.introPager.registerOnPageChangeCallback(pageChangeCallback)
TabLayoutMediator(
- binding.introTabs, binding.introPager
+ binding.introTabs,
+ binding.introPager,
) { tab, position -> tab.text = pages[position] }.attach()
binding.nextButton.setOnClickListener {
binding.apply {
- if (introPager.currentItem != pages.size - 1)
+ if (introPager.currentItem != pages.size - 1) {
introPager.currentItem++
+ }
}
}
@@ -113,59 +223,136 @@ class AuthActivity : AppCompatActivity() {
}
private fun logoutFailed() {
+ Timber.e("Google sign-in failed - showing error message to user")
Toast.makeText(this, getString(R.string.sign_in_fail), Toast.LENGTH_LONG).show()
binding.loadingView.visibility = View.GONE
binding.introPager.currentItem = 0
loginClick = false
}
- private fun saveInfo(token: String?, uid: String?) {
+ private fun saveInfo(
+ token: String?,
+ uid: String?,
+ ) {
with(sharedPref.edit()) {
- putString("sign_in_method", "Google")
+ putString("simethod", "Google")
putString(TOKEN, token)
putString(UID, uid)
apply()
}
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ override fun onActivityResult(
+ requestCode: Int,
+ resultCode: Int,
+ data: Intent?,
+ ) {
super.onActivityResult(requestCode, resultCode, data)
+ Timber.d("Activity Result - requestCode: $requestCode, resultCode: $resultCode")
if (requestCode == SIGNIN) {
val task: Task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
val account = task.getResult(ApiException::class.java)
+ Timber.d("Google sign-in account: $account")
if (account != null) {
+ Timber.d("Google sign-in successful, proceeding with Firebase auth")
firebaseAuthWithGoogle(account)
+ } else {
+ Timber.e("Google sign-in account is null")
+ logoutFailed()
}
} catch (e: ApiException) {
+ Timber.e("Google sign-in failed with ApiException: ${e.message}, statusCode: ${e.statusCode}")
+ logoutFailed()
+ } catch (e: Exception) {
+ Timber.e("Google sign-in failed with Exception: ${e.message}")
logoutFailed()
}
}
}
private fun firebaseAuthWithGoogle(acct: GoogleSignInAccount) {
+ Timber.d("Starting Firebase authentication with Google account: ${acct.email}")
val credential = GoogleAuthProvider.getCredential(acct.idToken, null)
- firebaseAuth.signInWithCredential(credential).addOnCompleteListener {
- if (it.isSuccessful) {
- loginClick = true
- val uid = firebaseAuth.currentUser?.uid
- saveInfo(acct.idToken, uid)
- val intent = Intent(this, InstructionsActivity::class.java)
+
+ firebaseAuth
+ .signInWithCredential(credential)
+ .addOnCompleteListener { authResult ->
+ if (authResult.isSuccessful) {
+ loginClick = true
+ val uid = firebaseAuth.currentUser?.uid
+ val email = firebaseAuth.currentUser?.email
+ Timber.d("Firebase authentication successful - uid: $uid, email: $email")
+ saveInfo(acct.idToken, uid)
+
+ // Quick maintenance check for new login
+ checkMaintenanceBeforeProceed()
+ } else {
+ Timber.e("Firebase authentication failed: ${authResult.exception?.message}")
+ logoutFailed()
+ }
+ }.addOnFailureListener { exception ->
+ Timber.e("Firebase authentication failed with exception: ${exception.message}")
+ logoutFailed()
+ }
+ }
+
+ private fun checkMaintenanceBeforeProceed() {
+ MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance ->
+ if (isUnderMaintenance) {
binding.loadingView.visibility = View.GONE
+ val intent = Intent(this, MaintenanceActivity::class.java)
startActivity(intent)
finish()
} else {
- logoutFailed()
+ authViewModel.signInAndGetTimeTable("", "", firebaseAuth.currentUser?.uid ?: "", "")
+ leadToNextPage()
}
}
}
- override fun onBackPressed() {
- binding.apply {
- if (introPager.currentItem == 0 || loginClick) {
- super.onBackPressed()
+ private fun leadToNextPage() {
+ authViewModel.signInResponse.observe(this) {
+ if (it != null) {
+ Timber.d("here--$it")
+ sharedPref.edit { putString(Constants.COMMUNITY_USERNAME, it.username) }
+ sharedPref.edit { putString(Constants.COMMUNITY_TOKEN, it.token) }
+ sharedPref.edit { putString(Constants.COMMUNITY_NAME, it.name) }
+ sharedPref.edit { putString(Constants.COMMUNITY_PICTURE, it.picture) }
+ sharedPref.edit { putString(Constants.COMMUNITY_CAMPUS, it.campus) }
} else {
- introPager.currentItem--
+ val intent = Intent(this, AddInfoActivity::class.java)
+ binding.loadingView.visibility = View.GONE
+ startActivity(intent)
+ finish()
+ }
+ }
+
+ authViewModel.user.observe(this) {
+ if (it != null) {
+ val timetableDays = it.timetable?.data
+ if (!timetableDays?.Monday.isNullOrEmpty() ||
+ !timetableDays?.Tuesday.isNullOrEmpty() ||
+ !timetableDays?.Wednesday.isNullOrEmpty() ||
+ !timetableDays?.Thursday.isNullOrEmpty() ||
+ !timetableDays?.Friday.isNullOrEmpty() ||
+ !timetableDays?.Saturday.isNullOrEmpty() ||
+ !timetableDays?.Sunday.isNullOrEmpty()
+ ) {
+ sharedPref
+ .edit {
+ putBoolean(Constants.COMMUNITY_TIMETABLE_AVAILABLE, true)
+ }
+ val intent = Intent(this, HomeComposeActivity::class.java)
+ startActivity(intent)
+ finish()
+ binding.loadingView.visibility = View.GONE
+ } else {
+ val intent = Intent(this, InstructionsActivity::class.java)
+ startActivity(intent)
+ finish()
+ binding.loadingView.visibility = View.GONE
+ }
}
}
}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/HomeActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/HomeActivity.kt
index f529585..c8ba281 100644
--- a/app/src/main/java/com/dscvit/vitty/activity/HomeActivity.kt
+++ b/app/src/main/java/com/dscvit/vitty/activity/HomeActivity.kt
@@ -1,28 +1,492 @@
package com.dscvit.vitty.activity
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
import android.os.Bundle
+import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.OvershootInterpolator
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.FragmentActivity
+import androidx.navigation.NavOptions
import androidx.navigation.findNavController
-import androidx.navigation.ui.setupWithNavController
import com.dscvit.vitty.R
import com.dscvit.vitty.databinding.ActivityHomeBinding
-import com.google.android.material.bottomnavigation.BottomNavigationView
class HomeActivity : FragmentActivity() {
-
private lateinit var binding: ActivityHomeBinding
+ private var selectedBackground: View? = null
+ private var currentSelectedId = -1
+ private var isAnimating = false
+ private var isNavigating = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
- val navView: BottomNavigationView = binding.navView
-
val navController = findNavController(R.id.nav_host_fragment_activity_main)
- navView.setupWithNavController(navController)
+ fun createNavOptions(): NavOptions =
+ NavOptions
+ .Builder()
+ .setEnterAnim(R.anim.crossfade_in)
+ .setExitAnim(R.anim.crossfade_out)
+ .setPopEnterAnim(R.anim.crossfade_in)
+ .setPopExitAnim(R.anim.crossfade_out)
+ .build()
+
+ fun navigateIfNeeded(destinationId: Int) {
+ if (navController.currentDestination?.id != destinationId && !isNavigating) {
+ isNavigating = true
+
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ navController.navigate(destinationId, null, createNavOptions())
+
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ isNavigating = false
+ }, 250)
+ }, 50)
+ }
+ }
+
+ fun findImageViewInLayout(layout: LinearLayout): ImageView? {
+ for (i in 0 until layout.childCount) {
+ val child = layout.getChildAt(i)
+ if (child is ImageView) {
+ return child
+ }
+ }
+ return null
+ }
+
+ fun animateIconBounce(layout: LinearLayout) {
+ val imageView = findImageViewInLayout(layout) ?: return
+
+ val scaleX =
+ ObjectAnimator.ofFloat(imageView, "scaleX", 1f, 1.15f, 1f).apply {
+ duration = 250
+ interpolator = OvershootInterpolator(1.0f)
+ }
+ val scaleY =
+ ObjectAnimator.ofFloat(imageView, "scaleY", 1f, 1.15f, 1f).apply {
+ duration = 250
+ interpolator = OvershootInterpolator(1.0f)
+ }
+
+ AnimatorSet().apply {
+ playTogether(scaleX, scaleY)
+ start()
+ }
+ }
+
+ fun animateTextSlideIn(
+ textView: TextView,
+ delay: Long = 0,
+ ) {
+ textView.alpha = 0f
+ textView.translationX = 10f
+ textView.scaleX = 0.95f
+ textView.visibility = View.VISIBLE
+
+ val slideAnimator =
+ ObjectAnimator.ofFloat(textView, "translationX", 10f, 0f).apply {
+ duration = 200
+ startDelay = delay
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val fadeAnimator =
+ ObjectAnimator.ofFloat(textView, "alpha", 0f, 1f).apply {
+ duration = 150
+ startDelay = delay
+ }
+ val scaleAnimator =
+ ObjectAnimator.ofFloat(textView, "scaleX", 0.95f, 1f).apply {
+ duration = 200
+ startDelay = delay
+ interpolator = OvershootInterpolator(0.6f)
+ }
+
+ AnimatorSet().apply {
+ playTogether(slideAnimator, fadeAnimator, scaleAnimator)
+ start()
+ }
+ }
+
+ fun animateTextSlideOut(
+ textView: TextView,
+ onComplete: () -> Unit = {},
+ ) {
+ if (textView.visibility != View.VISIBLE) {
+ onComplete()
+ return
+ }
+
+ val slideAnimator =
+ ObjectAnimator.ofFloat(textView, "translationX", 0f, -10f).apply {
+ duration = 100
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val fadeAnimator =
+ ObjectAnimator.ofFloat(textView, "alpha", 1f, 0f).apply {
+ duration = 100
+ }
+
+ AnimatorSet().apply {
+ playTogether(slideAnimator, fadeAnimator)
+ addListener(
+ object : android.animation.AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: android.animation.Animator) {
+ textView.visibility = View.GONE
+ textView.translationX = 0f
+ textView.alpha = 1f
+ textView.scaleX = 1f
+ onComplete()
+ }
+
+ override fun onAnimationCancel(animation: android.animation.Animator) {
+ textView.visibility = View.GONE
+ textView.translationX = 0f
+ textView.alpha = 1f
+ textView.scaleX = 1f
+ onComplete()
+ }
+ },
+ )
+ start()
+ }
+ }
+
+ fun animatePillMovement(
+ fromView: View?,
+ toView: View,
+ onComplete: () -> Unit = {},
+ ) {
+ binding.navAcademics.setBackgroundResource(0)
+ binding.navTimetable.setBackgroundResource(0)
+ binding.navCommunity.setBackgroundResource(0)
+
+ binding.navAcademics.elevation = 0f
+ binding.navTimetable.elevation = 0f
+ binding.navCommunity.elevation = 0f
+
+ if (fromView == null) {
+ toView.setBackgroundResource(R.drawable.bg_nav_item_selected)
+ toView.alpha = 0f
+ toView.scaleX = 0.95f
+ toView.scaleY = 0.95f
+
+ val fadeAnim =
+ ObjectAnimator.ofFloat(toView, "alpha", 0f, 1f).apply {
+ duration = 150
+ }
+ val scaleXAnim =
+ ObjectAnimator.ofFloat(toView, "scaleX", 0.95f, 1f).apply {
+ duration = 180
+ interpolator = OvershootInterpolator(0.8f)
+ }
+ val scaleYAnim =
+ ObjectAnimator.ofFloat(toView, "scaleY", 0.95f, 1f).apply {
+ duration = 180
+ interpolator = OvershootInterpolator(0.8f)
+ }
+ val elevationAnim =
+ ObjectAnimator.ofFloat(toView, "elevation", 0f, 8f).apply {
+ duration = 180
+ interpolator = DecelerateInterpolator()
+ }
+
+ AnimatorSet().apply {
+ playTogether(fadeAnim, scaleXAnim, scaleYAnim, elevationAnim)
+ addListener(
+ object : android.animation.AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: android.animation.Animator) {
+ onComplete()
+ }
+
+ override fun onAnimationCancel(animation: android.animation.Animator) {
+ onComplete()
+ }
+ },
+ )
+ start()
+ }
+ return
+ }
+
+ toView.setBackgroundResource(R.drawable.bg_nav_item_selected)
+
+ val scaleX =
+ ObjectAnimator.ofFloat(toView, "scaleX", 1f, 1.03f, 1f).apply {
+ duration = 150
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val scaleY =
+ ObjectAnimator.ofFloat(toView, "scaleY", 1f, 1.03f, 1f).apply {
+ duration = 150
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val elevationAnim =
+ ObjectAnimator.ofFloat(toView, "elevation", 0f, 8f).apply {
+ duration = 150
+ interpolator = DecelerateInterpolator()
+ }
+
+ AnimatorSet().apply {
+ playTogether(scaleX, scaleY, elevationAnim)
+ addListener(
+ object : android.animation.AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: android.animation.Animator) {
+ onComplete()
+ }
+
+ override fun onAnimationCancel(animation: android.animation.Animator) {
+ onComplete()
+ }
+ },
+ )
+ start()
+ }
+ }
+
+ fun highlightSelectedTab(selectedId: Int) {
+ if (currentSelectedId == selectedId) return
+
+ if (isAnimating) {
+ binding.textAcademics.clearAnimation()
+ binding.textTimetable.clearAnimation()
+ binding.textCommunity.clearAnimation()
+ binding.navAcademics.clearAnimation()
+ binding.navTimetable.clearAnimation()
+ binding.navCommunity.clearAnimation()
+ }
+
+ isAnimating = true
+
+ val (newSelectedView, newTextView) =
+ when (selectedId) {
+ R.id.navigation_academics -> Pair(binding.navAcademics, binding.textAcademics)
+ R.id.navigation_schedule -> Pair(binding.navTimetable, binding.textTimetable)
+ R.id.navigation_connect -> Pair(binding.navCommunity, binding.textCommunity)
+ else -> {
+ isAnimating = false
+ return
+ }
+ }
+
+ val allTextViews = listOf(binding.textAcademics, binding.textTimetable, binding.textCommunity)
+ val visibleTextViews = allTextViews.filter { it.isVisible && it != newTextView }
+
+ var completedOperations = 0
+ val totalOperations = 1 + visibleTextViews.size
+
+ fun onOperationComplete() {
+ completedOperations++
+ if (completedOperations >= totalOperations) {
+ isAnimating = false
+ }
+ }
+
+ animatePillMovement(selectedBackground, newSelectedView) {
+ onOperationComplete()
+ }
+
+ animateTextSlideIn(newTextView, delay = 25)
+
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ animateIconBounce(newSelectedView)
+ }, 50)
+
+ if (visibleTextViews.isEmpty()) {
+ onOperationComplete()
+ } else {
+ visibleTextViews.forEach { textView ->
+ animateTextSlideOut(textView) {
+ onOperationComplete()
+ }
+ }
+ }
+
+ selectedBackground = newSelectedView
+ currentSelectedId = selectedId
+ }
+
+ fun animateButtonPress(view: View) {
+ val scaleDown =
+ ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f).apply {
+ duration = 60
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val scaleDownY =
+ ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f).apply {
+ duration = 60
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val elevationDown =
+ ObjectAnimator.ofFloat(view, "elevation", view.elevation, view.elevation * 0.5f).apply {
+ duration = 60
+ }
+ val scaleUp =
+ ObjectAnimator.ofFloat(view, "scaleX", 0.95f, 1f).apply {
+ duration = 100
+ interpolator = OvershootInterpolator(1.2f)
+ }
+ val scaleUpY =
+ ObjectAnimator.ofFloat(view, "scaleY", 0.95f, 1f).apply {
+ duration = 100
+ interpolator = OvershootInterpolator(1.2f)
+ }
+ val elevationUp =
+ ObjectAnimator.ofFloat(view, "elevation", view.elevation * 0.5f, view.elevation).apply {
+ duration = 100
+ interpolator = OvershootInterpolator(0.6f)
+ }
+
+ AnimatorSet().apply {
+ play(scaleDown).with(scaleDownY).with(elevationDown)
+ play(scaleUp).after(scaleDown).with(scaleUpY).with(elevationUp)
+ start()
+ }
+ }
+
+ binding.navAcademics.setOnClickListener {
+ if (isAnimating || isNavigating) return@setOnClickListener
+ try {
+ it.performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
+ } catch (e: Exception) {
+ }
+ animateButtonPress(it)
+ navigateIfNeeded(R.id.navigation_academics)
+ }
+
+ binding.navTimetable.setOnClickListener {
+ if (isAnimating || isNavigating) return@setOnClickListener
+ try {
+ it.performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
+ } catch (e: Exception) {
+ }
+ animateButtonPress(it)
+ navigateIfNeeded(R.id.navigation_schedule)
+ }
+
+ binding.navCommunity.setOnClickListener {
+ if (isAnimating || isNavigating) return@setOnClickListener
+ try {
+ it.performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
+ } catch (e: Exception) {
+ }
+ animateButtonPress(it)
+ navigateIfNeeded(R.id.navigation_connect)
+ }
+
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ when (destination.id) {
+ R.id.allRequestFragment,
+ R.id.friendFragment,
+ R.id.searchFragment,
+ R.id.navigation_requests,
+ R.id.coursePageFragment,
+ R.id.noteFragment,
+ -> {
+ hideBottomNavSafely()
+ }
+
+ R.id.navigation_academics -> {
+ showBottomNavSafely()
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ highlightSelectedTab(R.id.navigation_academics)
+ }, 25)
+ }
+
+ R.id.navigation_schedule -> {
+ showBottomNavSafely()
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ highlightSelectedTab(R.id.navigation_schedule)
+ }, 25)
+ }
+
+ R.id.navigation_connect -> {
+ showBottomNavSafely()
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+ highlightSelectedTab(R.id.navigation_connect)
+ }, 25)
+ }
+
+ else -> {
+ hideBottomNavSafely()
+ }
+ }
+ }, 50)
+ }
+ }
+
+ private fun showBottomNavSafely() {
+ if (binding.customBottomNav.visibility != View.VISIBLE) {
+ binding.customBottomNav.visibility = View.VISIBLE
+ binding.customBottomNav.translationY = 200f
+ binding.customBottomNav.alpha = 0f
+ binding.customBottomNav.scaleY = 0.9f
+
+ val slideUp =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "translationY", 200f, 0f).apply {
+ duration = 300
+ interpolator = DecelerateInterpolator(1.0f)
+ }
+ val fadeIn =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "alpha", 0f, 1f).apply {
+ duration = 250
+ startDelay = 25
+ }
+ val scaleUp =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "scaleY", 0.9f, 1f).apply {
+ duration = 280
+ interpolator = OvershootInterpolator(0.4f)
+ startDelay = 50
+ }
+
+ AnimatorSet().apply {
+ playTogether(slideUp, fadeIn, scaleUp)
+ start()
+ }
+ }
}
+ private fun hideBottomNavSafely() {
+ if (binding.customBottomNav.visibility == View.VISIBLE) {
+ val slideDown =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "translationY", 0f, 200f).apply {
+ duration = 250
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ val fadeOut =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "alpha", 1f, 0f).apply {
+ duration = 200
+ }
+ val scaleDown =
+ ObjectAnimator.ofFloat(binding.customBottomNav, "scaleY", 1f, 0.9f).apply {
+ duration = 220
+ interpolator = AccelerateDecelerateInterpolator()
+ }
+ AnimatorSet().apply {
+ playTogether(slideDown, fadeOut, scaleDown)
+ addListener(
+ object : android.animation.AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: android.animation.Animator) {
+ binding.customBottomNav.visibility = View.GONE
+ binding.customBottomNav.translationY = 0f
+ binding.customBottomNav.scaleY = 1f
+ }
+ },
+ )
+ start()
+ }
+ }
+ }
}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt
new file mode 100644
index 0000000..d891ee7
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt
@@ -0,0 +1,247 @@
+package com.dscvit.vitty.activity
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.core.content.edit
+import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.FragmentActivity
+import com.dscvit.vitty.R
+import com.dscvit.vitty.databinding.ActivityHomeComposeBinding
+import com.dscvit.vitty.ui.main.MainComposeApp
+import com.dscvit.vitty.util.Constants
+import com.dscvit.vitty.util.Constants.PREF_LAST_REVIEW_REQUEST
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.play.core.appupdate.AppUpdateInfo
+import com.google.android.play.core.appupdate.AppUpdateManager
+import com.google.android.play.core.appupdate.AppUpdateManagerFactory
+import com.google.android.play.core.appupdate.AppUpdateOptions
+import com.google.android.play.core.install.InstallStateUpdatedListener
+import com.google.android.play.core.install.model.AppUpdateType
+import com.google.android.play.core.install.model.InstallStatus
+import com.google.android.play.core.install.model.UpdateAvailability
+import com.google.android.play.core.review.ReviewInfo
+import com.google.android.play.core.review.ReviewManager
+import com.google.android.play.core.review.ReviewManagerFactory
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+
+class HomeComposeActivity : FragmentActivity() {
+ private lateinit var binding: ActivityHomeComposeBinding
+ private lateinit var prefs: SharedPreferences
+ private lateinit var appUpdateManager: AppUpdateManager
+ private lateinit var reviewManager: ReviewManager
+
+ private var reviewInfo: ReviewInfo? = null
+
+ companion object {
+ private val REVIEW_INTERVAL_MILLIS = TimeUnit.DAYS.toMillis(30)
+ private const val TAG = "HomeComposeActivity"
+ }
+
+ private val updateResultLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.StartIntentSenderForResult(),
+ ) { result: ActivityResult ->
+ when (result.resultCode) {
+ RESULT_OK -> {
+ Timber.d("$TAG:Update flow completed successfully")
+ }
+ RESULT_CANCELED -> {
+ Timber.d("$TAG:Update flow was cancelled by user")
+ }
+ else -> {
+ Timber.d("$TAG:Update flow failed with result code: ${result.resultCode}")
+ }
+ }
+ }
+
+ private val installStateUpdatedListener: InstallStateUpdatedListener =
+ InstallStateUpdatedListener { state ->
+ when (state.installStatus()) {
+ InstallStatus.DOWNLOADED -> {
+ showUpdateDownloadedSnackbar()
+ }
+ InstallStatus.INSTALLED -> {
+ appUpdateManager.unregisterListener(installStateUpdatedListener)
+ }
+ else -> {
+ Timber.d("$TAG:Install status: ${state.installStatus()}")
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_home_compose)
+ prefs = getSharedPreferences(Constants.USER_INFO, 0)
+ appUpdateManager = AppUpdateManagerFactory.create(this)
+ reviewManager = ReviewManagerFactory.create(this)
+
+ binding.composeView.apply {
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@HomeComposeActivity),
+ )
+ setContent { MainComposeApp() }
+ }
+
+ appUpdateManager.registerListener(installStateUpdatedListener)
+
+ checkForAppUpdate()
+ checkForReviewRequest()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ appUpdateManager.appUpdateInfo
+ .addOnSuccessListener { appUpdateInfo ->
+ Timber.d(
+ "$TAG:onResume - Update availability: ${appUpdateInfo.updateAvailability()}",
+ )
+ Timber.d("$TAG:onResume - Install status: ${appUpdateInfo.installStatus()}")
+
+ if (appUpdateInfo.updateAvailability() ==
+ UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
+ ) {
+ Timber.d("$TAG:Resuming in-progress update")
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ updateResultLauncher,
+ AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
+ )
+ }
+
+ if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
+ showUpdateDownloadedSnackbar()
+ }
+ }.addOnFailureListener { exception ->
+ Timber.e("$TAG:Failed to get app update info in onResume$exception")
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ appUpdateManager.unregisterListener(installStateUpdatedListener)
+ }
+
+ private fun checkForAppUpdate() {
+ Timber.d("$TAG:Checking for app updates...")
+
+ appUpdateManager.appUpdateInfo
+ .addOnSuccessListener { appUpdateInfo ->
+ Timber.d("$TAG:Update availability: ${appUpdateInfo.updateAvailability()}")
+ Timber.d("$TAG:Update priority: ${appUpdateInfo.updatePriority()}")
+ Timber.d(
+ "$TAG:Client version staleness days: ${appUpdateInfo.clientVersionStalenessDays()}",
+ )
+ Timber.d("$TAG:Available version code: ${appUpdateInfo.availableVersionCode()}")
+ Timber.d("$TAG:Install status: ${appUpdateInfo.installStatus()}")
+
+ when {
+ appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
+ appUpdateInfo.updatePriority() >= 4 &&
+ appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> {
+ Timber.d("$TAG:Starting immediate update (high priority)")
+ startImmediateUpdate(appUpdateInfo)
+ }
+ appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
+ appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> {
+ val stalenessDays = appUpdateInfo.clientVersionStalenessDays() ?: 0
+ Timber.d("$TAG:Update staleness: $stalenessDays days")
+
+ if (stalenessDays >= 1) {
+ Timber.d("$TAG:Starting flexible update (staleness criteria met)")
+ startFlexibleUpdate(appUpdateInfo)
+ } else {
+ Timber.d("$TAG:Update available but staleness criteria not met")
+ }
+ }
+ appUpdateInfo.updateAvailability() ==
+ UpdateAvailability.UPDATE_AVAILABLE -> {
+ Timber.d("$TAG:Update available but no update type allowed")
+ Timber.d(
+ "$TAG:Immediate allowed: ${appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)}",
+ )
+ Timber.d(
+ "$TAG:Flexible allowed: ${appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)}",
+ )
+ }
+ else -> {
+ Timber.d("$TAG:No update available or update not applicable")
+ }
+ }
+ }.addOnFailureListener { exception ->
+ Timber.e("$TAG:Failed to check for app updates:$exception")
+ }
+ }
+
+ private fun startImmediateUpdate(appUpdateInfo: AppUpdateInfo) {
+ Timber.d("Launching immediate update flow")
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ updateResultLauncher,
+ AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
+ )
+ }
+
+ private fun startFlexibleUpdate(appUpdateInfo: AppUpdateInfo) {
+ Timber.d("Launching flexible update flow")
+ appUpdateManager.startUpdateFlowForResult(
+ appUpdateInfo,
+ updateResultLauncher,
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
+ )
+ }
+
+ private fun showUpdateDownloadedSnackbar() {
+ Timber.d("Showing update downloaded snackbar")
+ Snackbar
+ .make(
+ binding.root,
+ "An update has been downloaded.",
+ Snackbar.LENGTH_INDEFINITE,
+ ).apply {
+ setAction("RESTART") {
+ Timber.d("User clicked restart - completing update")
+ appUpdateManager.completeUpdate()
+ }
+ show()
+ }
+ }
+
+ private fun checkForReviewRequest() {
+ val lastReviewRequest = prefs.getLong(PREF_LAST_REVIEW_REQUEST, 0)
+ val currentTime = System.currentTimeMillis()
+
+ if (currentTime - lastReviewRequest >= REVIEW_INTERVAL_MILLIS) {
+ requestReviewInfo()
+ }
+ }
+
+ private fun requestReviewInfo() {
+ val request = reviewManager.requestReviewFlow()
+ request.addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ reviewInfo = task.result
+ launchReviewFlow()
+ } else {
+ Timber.e("$TAG:Failed to request review info:${task.exception}")
+ }
+ }
+ }
+
+ private fun launchReviewFlow() {
+ reviewInfo?.let { info ->
+ val flow = reviewManager.launchReviewFlow(this, info)
+ flow.addOnCompleteListener {
+ prefs.edit { putLong(PREF_LAST_REVIEW_REQUEST, System.currentTimeMillis()) }
+ reviewInfo = null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/InstructionsActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/InstructionsActivity.kt
index d23fc02..956b9ca 100755
--- a/app/src/main/java/com/dscvit/vitty/activity/InstructionsActivity.kt
+++ b/app/src/main/java/com/dscvit/vitty/activity/InstructionsActivity.kt
@@ -15,12 +15,15 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
import com.dscvit.vitty.BuildConfig
import com.dscvit.vitty.R
import com.dscvit.vitty.databinding.ActivityInstructionsBinding
import com.dscvit.vitty.receiver.AlarmReceiver
+import com.dscvit.vitty.ui.auth.AuthViewModel
import com.dscvit.vitty.util.ArraySaverLoader.loadArray
import com.dscvit.vitty.util.ArraySaverLoader.saveArray
+import com.dscvit.vitty.util.Constants
import com.dscvit.vitty.util.Constants.ALARM_INTENT
import com.dscvit.vitty.util.Constants.EXAM_MODE
import com.dscvit.vitty.util.Constants.GROUP_ID
@@ -41,8 +44,8 @@ import timber.log.Timber
import java.util.Date
class InstructionsActivity : AppCompatActivity() {
-
private lateinit var binding: ActivityInstructionsBinding
+ private lateinit var authViewModel: AuthViewModel
private val days =
listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
private lateinit var prefs: SharedPreferences
@@ -54,12 +57,14 @@ class InstructionsActivity : AppCompatActivity() {
binding = DataBindingUtil.setContentView(this, R.layout.activity_instructions)
prefs = getSharedPreferences(USER_INFO, 0)
uid = prefs.getString(UID, "").toString()
-
+ authViewModel = ViewModelProvider(this)[AuthViewModel::class.java]
setupToolbar()
setGDSCVITChannel()
binding.doneButton.setOnClickListener {
- setupDoneButton()
+ val token = prefs.getString(Constants.COMMUNITY_TOKEN, null)
+ val username = prefs.getString(Constants.COMMUNITY_USERNAME, null)
+ setupDoneButton(token, username)
}
}
@@ -67,20 +72,62 @@ class InstructionsActivity : AppCompatActivity() {
super.onStart()
if (prefs.getInt(UPDATE, 0) == 1) {
createNotificationChannels()
- Toast.makeText(this, getString(R.string.updated), Toast.LENGTH_SHORT)
+ Toast
+ .makeText(this, getString(R.string.updated), Toast.LENGTH_SHORT)
.show()
}
if (prefs.getInt(TIMETABLE_AVAILABLE, 0) == 1) {
setAlarm()
- val intent = Intent(this, HomeActivity::class.java)
+ val intent = Intent(this, HomeComposeActivity::class.java)
startActivity(intent)
finish()
}
}
- private fun setupDoneButton() {
+ private fun setupDoneButton(
+ token: String?,
+ username: String?,
+ ) {
binding.loadingView.visibility = View.VISIBLE
- db.collection("users")
+
+ Timber.d("done button clicked")
+
+ if (token != null && username != null) {
+ authViewModel.getUserWithTimeTable(token, username)
+ } else {
+ Toast
+ .makeText(this, "Please login again", Toast.LENGTH_LONG)
+ .show()
+ }
+
+ authViewModel.user.observe(this) {
+ Timber.d("user: $it")
+ if (it != null) {
+ val timetableDays = it.timetable?.data
+ if (!timetableDays?.Monday.isNullOrEmpty() ||
+ !timetableDays?.Tuesday.isNullOrEmpty() ||
+ !timetableDays?.Wednesday.isNullOrEmpty() ||
+ !timetableDays?.Thursday.isNullOrEmpty() ||
+ !timetableDays?.Friday.isNullOrEmpty() ||
+ !timetableDays?.Saturday.isNullOrEmpty() ||
+ !timetableDays?.Sunday.isNullOrEmpty()
+ ) {
+ binding.loadingView.visibility = View.GONE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannels()
+ } else {
+ tellUpdated()
+ }
+ } else {
+ binding.loadingView.visibility = View.GONE
+ Toast
+ .makeText(this, getString(R.string.follow_instructions), Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+
+ /*db.collection("users")
.document(uid)
.get()
.addOnSuccessListener { document ->
@@ -95,7 +142,7 @@ class InstructionsActivity : AppCompatActivity() {
Toast.makeText(this, getString(R.string.follow_instructions), Toast.LENGTH_LONG)
.show()
}
- }
+ }*/
}
private fun createNotificationChannels() {
@@ -109,7 +156,8 @@ class InstructionsActivity : AppCompatActivity() {
}
val newNotifChannels: ArrayList = ArrayList()
for (day in days) {
- db.collection("users")
+ db
+ .collection("users")
.document(uid)
.collection("timetable")
.document(day)
@@ -124,17 +172,17 @@ class InstructionsActivity : AppCompatActivity() {
this,
cn,
"Course Code: $cc",
- GROUP_ID
+ GROUP_ID,
)
newNotifChannels.add(cn)
Timber.d(cn)
}
saveArray(newNotifChannels, NOTIFICATION_CHANNELS, this)
- if (day == "sunday")
+ if (day == "sunday") {
tellUpdated()
- }
- .addOnFailureListener { e ->
+ }
+ }.addOnFailureListener { e ->
Timber.d("Error: $e")
}
}
@@ -143,11 +191,13 @@ class InstructionsActivity : AppCompatActivity() {
private fun tellUpdated() {
prefs.edit().putInt(TIMETABLE_AVAILABLE, 1).apply()
prefs.edit().putInt(UPDATE, 0).apply()
- val updated = hashMapOf(
- "isTimetableAvailable" to true,
- "isUpdated" to false
- )
- db.collection("users")
+ val updated =
+ hashMapOf(
+ "isTimetableAvailable" to true,
+ "isUpdated" to false,
+ )
+ db
+ .collection("users")
.document(uid)
.set(updated)
.addOnSuccessListener {
@@ -155,21 +205,21 @@ class InstructionsActivity : AppCompatActivity() {
UtilFunctions.reloadWidgets(this)
val pm: PowerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
- Toast.makeText(
- this,
- "Please turn off the Battery Optimization Settings for VITTY to receive notifications on time.",
- Toast.LENGTH_LONG
- ).show()
+ Toast
+ .makeText(
+ this,
+ "Please turn off the Battery Optimization Settings for VITTY to receive notifications on time.",
+ Toast.LENGTH_LONG,
+ ).show()
val pmIntent = Intent()
pmIntent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
startActivity(pmIntent)
} else {
- val intent = Intent(this, HomeActivity::class.java)
+ val intent = Intent(this, HomeComposeActivity::class.java)
startActivity(intent)
finish()
}
- }
- .addOnFailureListener { e ->
+ }.addOnFailureListener { e ->
Timber.d("Error: $e")
}
}
@@ -179,13 +229,13 @@ class InstructionsActivity : AppCompatActivity() {
NotificationHelper.createNotificationGroup(
this,
getString(R.string.gdscvit),
- GROUP_ID_2
+ GROUP_ID_2,
)
NotificationHelper.createNotificationChannel(
this,
getString(R.string.default_notification_channel_name),
"Notifications from GDSC VIT",
- GROUP_ID_2
+ GROUP_ID_2,
)
prefs.edit {
putBoolean("gdscvitChannelCreated", true)
@@ -199,7 +249,7 @@ class InstructionsActivity : AppCompatActivity() {
NotificationHelper.createNotificationGroup(
this,
getString(R.string.notif_group),
- GROUP_ID
+ GROUP_ID,
)
prefs.edit {
putBoolean("groupCreated", true)
@@ -215,8 +265,10 @@ class InstructionsActivity : AppCompatActivity() {
val pendingIntent =
PendingIntent.getBroadcast(
- this, ALARM_INTENT, intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ this,
+ ALARM_INTENT,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
@@ -226,7 +278,7 @@ class InstructionsActivity : AppCompatActivity() {
AlarmManager.RTC_WAKEUP,
date,
(1000 * 60 * NOTIF_DELAY).toLong(),
- pendingIntent
+ pendingIntent,
)
prefs.edit {
@@ -244,6 +296,7 @@ class InstructionsActivity : AppCompatActivity() {
LogoutHelper.logout(this, this as Activity, prefs)
true
}
+
else -> false
}
}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt
new file mode 100644
index 0000000..1b4fef1
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt
@@ -0,0 +1,52 @@
+package com.dscvit.vitty.activity
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.FragmentActivity
+import com.dscvit.vitty.R
+import com.dscvit.vitty.databinding.ActivityMaintenanceBinding
+import com.dscvit.vitty.theme.VittyTheme
+import com.dscvit.vitty.ui.maintenance.MaintenanceScreen
+import com.dscvit.vitty.util.MaintenanceChecker
+
+class MaintenanceActivity : FragmentActivity() {
+
+ private lateinit var binding: ActivityMaintenanceBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_maintenance)
+
+ binding.composeView.apply {
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@MaintenanceActivity)
+ )
+ setContent {
+ VittyTheme {
+ MaintenanceScreen(
+ onRetryClick = { retryConnection() },
+ onExitClick = { exitApp() }
+ )
+ }
+ }
+ }
+ }
+
+ private fun retryConnection() {
+ MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance ->
+ if (!isUnderMaintenance) {
+ val intent = Intent(this, InstructionsActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ finish()
+ }
+ }
+ }
+
+ private fun exitApp() {
+ finishAffinity()
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/SettingsActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/SettingsActivity.kt
index 6292018..bcd4d4b 100755
--- a/app/src/main/java/com/dscvit/vitty/activity/SettingsActivity.kt
+++ b/app/src/main/java/com/dscvit/vitty/activity/SettingsActivity.kt
@@ -40,6 +40,7 @@ class SettingsActivity : AppCompatActivity() {
onBackPressed()
true
}
+
else -> false
}
}
diff --git a/app/src/main/java/com/dscvit/vitty/activity/VITEventsActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/VITEventsActivity.kt
index b91f085..1cb120a 100644
--- a/app/src/main/java/com/dscvit/vitty/activity/VITEventsActivity.kt
+++ b/app/src/main/java/com/dscvit/vitty/activity/VITEventsActivity.kt
@@ -41,6 +41,7 @@ class VITEventsActivity : AppCompatActivity() {
onBackPressed()
true
}
+
else -> false
}
}
diff --git a/app/src/main/java/com/dscvit/vitty/adapter/DayAdapter.kt b/app/src/main/java/com/dscvit/vitty/adapter/DayAdapter.kt
index bd7f139..dcc0370 100755
--- a/app/src/main/java/com/dscvit/vitty/adapter/DayAdapter.kt
+++ b/app/src/main/java/com/dscvit/vitty/adapter/DayAdapter.kt
@@ -5,7 +5,7 @@ import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.dscvit.vitty.ui.schedule.DayFragment
-class DayAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
+class DayAdapter(fa: Fragment, private val username: String?, private val isFriendsTimetable: Boolean = false) : FragmentStateAdapter(fa) {
private val numPages = 7
override fun getItemCount(): Int = numPages
@@ -13,6 +13,8 @@ class DayAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
override fun createFragment(position: Int): Fragment {
val bundle = Bundle()
bundle.putString("frag_id", position.toString())
+ bundle.putString("username", username)
+ bundle.putBoolean("isFriendsTimetable", isFriendsTimetable)
val fragment = DayFragment()
fragment.arguments = bundle
return fragment
diff --git a/app/src/main/java/com/dscvit/vitty/adapter/EventAdapter.kt b/app/src/main/java/com/dscvit/vitty/adapter/EventAdapter.kt
index 0ba0191..9d49995 100644
--- a/app/src/main/java/com/dscvit/vitty/adapter/EventAdapter.kt
+++ b/app/src/main/java/com/dscvit/vitty/adapter/EventAdapter.kt
@@ -27,8 +27,12 @@ class EventAdapter(private val dataSet: List) :
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- val date: Date = SimpleDateFormat("hh:mm z", Locale.getDefault()).parse("${dataSet[position].Time} IST") as Date
- val time: String = SimpleDateFormat("h:mm a", Locale.getDefault()).format(date).uppercase(Locale.ROOT)
+ val date: Date = SimpleDateFormat(
+ "hh:mm z",
+ Locale.getDefault()
+ ).parse("${dataSet[position].Time} IST") as Date
+ val time: String =
+ SimpleDateFormat("h:mm a", Locale.getDefault()).format(date).uppercase(Locale.ROOT)
viewHolder.eventName.text = dataSet[position].EventName
viewHolder.eventTime.text = time
}
diff --git a/app/src/main/java/com/dscvit/vitty/adapter/FriendAdapter.kt b/app/src/main/java/com/dscvit/vitty/adapter/FriendAdapter.kt
new file mode 100644
index 0000000..af8e94f
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/adapter/FriendAdapter.kt
@@ -0,0 +1,171 @@
+package com.dscvit.vitty.adapter
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import com.dscvit.vitty.R
+import com.dscvit.vitty.databinding.CardFriendBinding
+import com.dscvit.vitty.network.api.community.responses.user.UserResponse
+import com.dscvit.vitty.util.Effects.vibrateOnClick
+import timber.log.Timber
+
+class FriendAdapter(
+ dataList: List,
+ private val pinnedFriendAdapterListener: PinnedFriendAdapterListener
+) :
+ RecyclerView.Adapter() {
+
+
+ private val dataSet = pinFriendsOnTop(dataList).toMutableList()
+
+ class ViewHolder(private val binding: CardFriendBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ val friend_name = binding.friendName
+ val friend_class = binding.friendClass
+ val friend_status = binding.friendStatus
+ val friend_image = binding.icon
+ val pin = binding.pin
+
+ fun bind(data: UserResponse) {
+ binding.friendDetails = data
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.card_friend,
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = dataSet[holder.adapterPosition]
+ holder.bind(item)
+
+
+ /* val startTime: Date = item.startTime.toDate()
+ val simpleDateFormat = SimpleDateFormat("h:mm a", Locale.getDefault())
+ val sTime: String = simpleDateFormat.format(startTime).uppercase(Locale.ROOT)
+
+ val endTime: Date = item.endTime.toDate()
+ val eTime: String = simpleDateFormat.format(endTime).uppercase(Locale.ROOT)*/
+
+ /*val now = Calendar.getInstance()
+ val s = Calendar.getInstance()
+ s.time = startTime
+ val start = Calendar.getInstance()
+ start[Calendar.HOUR_OF_DAY] = s[Calendar.HOUR_OF_DAY]
+ start[Calendar.MINUTE] = s[Calendar.MINUTE]
+ val e = Calendar.getInstance()
+ e.time = endTime
+ val end = Calendar.getInstance()
+ end[Calendar.HOUR_OF_DAY] = e[Calendar.HOUR_OF_DAY]
+ end[Calendar.MINUTE] = e[Calendar.MINUTE]*/
+
+ holder.friend_status.text = item.current_status?.venue ?: "Free"
+ val status = item.current_status?.status
+ val course = item.current_status?.`class`
+
+ holder.friend_class.text =
+ if (status == null || status.lowercase() == "free" || status.lowercase() == "unknown") {
+ "Not in a class right now"
+ } else {
+ "$course"
+ }
+ val pinnedFriends = pinnedFriendAdapterListener.getPinnedFriends()
+ if (pinnedFriends.contains(item.username)) {
+ holder.pin.visibility = View.VISIBLE
+ } else {
+ holder.pin.visibility = View.GONE
+ }
+
+ holder.friend_image.load(item.picture) {
+ crossfade(true)
+ placeholder(R.drawable.ic_gdscvit)
+ error(R.drawable.ic_gdscvit)
+ }
+
+ holder.itemView.apply {
+ setOnClickListener {
+ val bundle = Bundle()
+ bundle.putString("username", item.username)
+ bundle.putString("name", item.name)
+ bundle.putString("profile_picture", item.picture)
+ bundle.putString("friend_status", item.friend_status)
+
+ findNavController().navigate(
+ R.id.action_navigation_community_to_friendFragment,
+ bundle
+ )
+ }
+ }
+
+ holder.itemView.apply {
+ setOnLongClickListener {
+ vibrateOnClick(context)
+ val updatedPinnedFriends = pinnedFriendAdapterListener.getPinnedFriends()
+ if (updatedPinnedFriends.contains(item.username)) {
+ if (pinnedFriendAdapterListener.unPinFriend(item.username)) {
+ holder.pin.visibility = View.GONE
+ notifyItemMoved(
+ holder.adapterPosition,
+ pinnedFriendAdapterListener.getPinnedFriends().size
+ )
+ Timber.d("Pinned Friends: ${pinnedFriendAdapterListener.getPinnedFriends()}")
+ }
+ } else {
+ if (pinnedFriendAdapterListener.pinFriend(item.username)) {
+ holder.pin.visibility = View.VISIBLE
+ notifyItemMoved(holder.adapterPosition, updatedPinnedFriends.size)
+ Timber.d("Pinned Friends: ${pinnedFriendAdapterListener.getPinnedFriends()}")
+ }
+
+ }
+ true
+ }
+ }
+
+
+ }
+
+ override fun getItemCount() = dataSet.size
+
+ private fun pinFriendsOnTop(dataSet: List): List {
+ val pinnedFriends = pinnedFriendAdapterListener.getPinnedFriends()
+ val pinnedFriendsList = mutableListOf()
+ val otherFriendsList = mutableListOf()
+ for (i in pinnedFriends) {
+ for (j in dataSet) {
+ if (i == j.username) {
+ pinnedFriendsList.add(j)
+ continue
+ }
+ }
+ }
+ for (i in dataSet) {
+ if (!pinnedFriends.contains(i.username)) {
+ pinnedFriendsList.add(i)
+ }
+ }
+
+ return pinnedFriendsList + otherFriendsList
+ }
+}
+
+
+interface PinnedFriendAdapterListener {
+ fun pinFriend(username: String): Boolean
+
+ fun unPinFriend(username: String): Boolean
+
+ fun getPinnedFriends(): List
+}
diff --git a/app/src/main/java/com/dscvit/vitty/adapter/PeriodAdapter.kt b/app/src/main/java/com/dscvit/vitty/adapter/PeriodAdapter.kt
index 761e008..0e54fee 100755
--- a/app/src/main/java/com/dscvit/vitty/adapter/PeriodAdapter.kt
+++ b/app/src/main/java/com/dscvit/vitty/adapter/PeriodAdapter.kt
@@ -3,6 +3,7 @@ package com.dscvit.vitty.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.dscvit.vitty.R
@@ -17,42 +18,44 @@ import java.util.Calendar
import java.util.Date
import java.util.Locale
-class PeriodAdapter(private val dataSet: ArrayList, private val day: Int) :
- RecyclerView.Adapter() {
-
- private var previousExpandedPosition = -1
- private var mExpandedPosition = -1
+class PeriodAdapter(
+ private val dataSet: ArrayList,
+ private val day: Int,
+) : RecyclerView.Adapter() {
private var active = -1
- class ViewHolder(private val binding: CardPeriodBinding) :
- RecyclerView.ViewHolder(binding.root) {
- val arrow = binding.arrowMoreInfo
- val moreInfo = binding.moreInfo
- val expandedBackground = binding.expandedBackground
+ class ViewHolder(
+ private val binding: CardPeriodBinding,
+ ) : RecyclerView.ViewHolder(binding.root) {
val activePeriod = binding.activePeriod
val periodTime = binding.periodTime
val classNav = binding.classNav
val classIdOnline = binding.classIdOnline
+ val periodCard = binding.periodCard
- // val courseCode = binding.courseCode
fun bind(data: PeriodDetails) {
binding.periodDetails = data
}
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): ViewHolder =
+ ViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.card_period,
parent,
- false
- )
+ false,
+ ),
)
- }
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val item = dataSet[position]
+ override fun onBindViewHolder(
+ holder: ViewHolder,
+ position: Int,
+ ) {
+ val item = dataSet[holder.adapterPosition]
holder.bind(item)
val startTime: Date = item.startTime.toDate()
@@ -85,8 +88,10 @@ class PeriodAdapter(private val dataSet: ArrayList, private val d
}
holder.apply {
- periodTime.text = "$sTime - $eTime"
+ periodTime.text = "$sTime - $eTime | ${item.slot}"
activePeriod.visibility = View.INVISIBLE
+ periodCard.strokeWidth = 0
+
classNav.apply {
setOnClickListener {
VITMap.openClassMap(classNav.context, item.roomNo)
@@ -96,7 +101,7 @@ class PeriodAdapter(private val dataSet: ArrayList, private val d
context,
"Room Number",
"ROOM_NUMBER_ITEM",
- item.roomNo
+ item.roomNo,
)
true
}
@@ -104,46 +109,26 @@ class PeriodAdapter(private val dataSet: ArrayList, private val d
}
if ((((day + 1) % 7) + 1) == now[Calendar.DAY_OF_WEEK]) {
- if ((start.before(now) && end.after(now)) || start.equals(now) || (start.after(now) && active == -1) || active == position) {
+ if ((start.before(now) && end.after(now)) ||
+ start.equals(now) ||
+ (start.after(now) && active == -1) ||
+ active == holder.adapterPosition
+ ) {
holder.activePeriod.visibility = View.VISIBLE
- active = position
+ holder.periodCard.strokeWidth = 2
+ holder.periodCard.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.translucent)
+ active = holder.adapterPosition
}
}
- val isExpanded = position == mExpandedPosition
- holder.apply {
- if (isExpanded) {
- expandedBackground.visibility = View.VISIBLE
- moreInfo.visibility = View.VISIBLE
-// courseCode.visibility = View.VISIBLE
- arrow.rotation = 180F
- } else {
- moreInfo.visibility = View.GONE
- expandedBackground.visibility = View.GONE
-// courseCode.visibility = View.GONE
- arrow.rotation = 0F
- }
- itemView.isActivated = isExpanded
- }
-
- if (isExpanded) previousExpandedPosition = position
-
holder.itemView.apply {
setOnClickListener {
vibrateOnClick(holder.itemView.context)
- mExpandedPosition = if (isExpanded) -1 else position
- notifyItemChanged(previousExpandedPosition)
- notifyItemChanged(position)
}
setOnLongClickListener {
- mExpandedPosition = position
- notifyItemChanged(previousExpandedPosition)
- notifyItemChanged(position)
true
}
}
-
-
}
override fun getItemCount() = dataSet.size
diff --git a/app/src/main/java/com/dscvit/vitty/adapter/SearchAdapter.kt b/app/src/main/java/com/dscvit/vitty/adapter/SearchAdapter.kt
new file mode 100644
index 0000000..23efe31
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/adapter/SearchAdapter.kt
@@ -0,0 +1,191 @@
+package com.dscvit.vitty.adapter
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.databinding.DataBindingUtil
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import com.dscvit.vitty.R
+import com.dscvit.vitty.databinding.CardRequestBinding
+import com.dscvit.vitty.network.api.community.responses.user.UserResponse
+import com.dscvit.vitty.ui.community.CommunityViewModel
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+class SearchAdapter(
+ dataSet: List,
+ private val token: String,
+ private val communityViewModel: CommunityViewModel,
+ private val isSearchMode: Boolean,
+ private val isAllReqPage: Boolean
+) :
+ RecyclerView.Adapter() {
+
+ private val mutableDataSet = dataSet.toMutableList()
+
+ class ViewHolder(private val binding: CardRequestBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ val name = binding.name
+ val actionLayout = binding.actionLayout
+ val pendingRequestLayout = binding.acceptRejectLayout
+ val sendRequestLayout = binding.sendRequestLayout
+ val sentLayout = binding.sentLayout
+ val accept = binding.accept
+ val reject = binding.reject
+ val sendRequest = binding.sendRequest
+ val image = binding.icon
+ val username = binding.username
+ fun bind(data: UserResponse) {
+ binding.personDetails = data
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.card_request,
+ parent,
+ false
+ )
+ )
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = mutableDataSet[holder.adapterPosition]
+ holder.bind(item)
+
+ if (isSearchMode) {
+ holder.actionLayout.visibility = View.GONE
+ }
+ when (item.friend_status) {
+ "received" -> {
+ holder.pendingRequestLayout.visibility = View.VISIBLE
+ holder.sendRequestLayout.visibility = View.GONE
+ holder.sentLayout.visibility = View.GONE
+ }
+
+ "sent" -> {
+ holder.pendingRequestLayout.visibility = View.GONE
+ holder.sendRequestLayout.visibility = View.GONE
+ holder.sentLayout.visibility = View.VISIBLE
+ }
+
+ else -> {
+ holder.pendingRequestLayout.visibility = View.GONE
+ holder.sendRequestLayout.visibility = View.VISIBLE
+ holder.sentLayout.visibility = View.GONE
+ }
+ }
+
+ holder.image.load(item.picture) {
+ crossfade(true)
+ placeholder(R.drawable.ic_gdscvit)
+ error(R.drawable.ic_gdscvit)
+ }
+
+
+ holder.accept.apply {
+ setOnClickListener {
+ communityViewModel.acceptRequest(token, item.username)
+ mutableDataSet.removeAt(holder.adapterPosition)
+ notifyItemRemoved(holder.adapterPosition)
+ notifyItemRangeChanged(holder.adapterPosition, mutableDataSet.size)
+ }
+ }
+
+ holder.reject.apply {
+ setOnClickListener {
+
+ val v: View = LayoutInflater
+ .from(context)
+ .inflate(R.layout.dialog_setup_complete, null)
+ val dialog = MaterialAlertDialogBuilder(context)
+ .setView(v)
+ .setBackground(
+ AppCompatResources.getDrawable(
+ context,
+ R.color.transparent
+ )
+ )
+ .create()
+ dialog.setCanceledOnTouchOutside(true)
+ dialog.show()
+
+ val skip = v.findViewById(R.id.skip)
+ val next = v.findViewById(R.id.next)
+ val title = v.findViewById(R.id.title)
+ val desc = v.findViewById(R.id.description)
+
+ title.text = "Reject Request"
+ desc.text = "Are you sure you want to reject this request?"
+ skip.text = "Cancel"
+ next.text = "Reject"
+
+ skip.setOnClickListener {
+ dialog.dismiss()
+ }
+
+ next.setOnClickListener {
+ communityViewModel.rejectRequest(token, item.username)
+ mutableDataSet.removeAt(holder.adapterPosition)
+ notifyItemRemoved(holder.adapterPosition)
+ notifyItemRangeChanged(holder.adapterPosition, mutableDataSet.size)
+ dialog.dismiss()
+ }
+
+ }
+ }
+
+ holder.sendRequest.apply {
+ setOnClickListener {
+ holder.sendRequestLayout.visibility = View.GONE
+ holder.sentLayout.visibility = View.VISIBLE
+ communityViewModel.sendRequest(token, item.username)
+ }
+ }
+
+
+
+ holder.itemView.apply {
+ setOnClickListener {
+ val bundle = Bundle()
+ bundle.putString("username", item.username)
+ bundle.putString("name", item.name)
+ bundle.putString("profile_picture", item.picture)
+ bundle.putString("friend_status", item.friend_status)
+
+ if (isSearchMode) {
+ findNavController().navigate(
+ R.id.action_searchFragment_to_friendFragment,
+ bundle
+ )
+ } else if (isAllReqPage) {
+ findNavController().navigate(
+ R.id.action_allRequestFragment_to_friendFragment,
+ bundle
+ )
+ } else {
+ findNavController().navigate(
+ R.id.action_navigation_requests_to_friendFragment,
+ bundle
+ )
+ }
+ }
+
+ }
+
+
+ }
+
+ override fun getItemCount() = mutableDataSet.size
+
+}
+
diff --git a/app/src/main/java/com/dscvit/vitty/data/converter/Converters.kt b/app/src/main/java/com/dscvit/vitty/data/converter/Converters.kt
new file mode 100644
index 0000000..715e212
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/converter/Converters.kt
@@ -0,0 +1,12 @@
+package com.dscvit.vitty.data.converter
+
+import androidx.room.TypeConverter
+import com.dscvit.vitty.ui.coursepage.models.NoteType
+
+class Converters {
+ @TypeConverter
+ fun fromNoteType(noteType: NoteType): String = noteType.name
+
+ @TypeConverter
+ fun toNoteType(noteType: String): NoteType = NoteType.valueOf(noteType)
+}
diff --git a/app/src/main/java/com/dscvit/vitty/data/dao/NoteDao.kt b/app/src/main/java/com/dscvit/vitty/data/dao/NoteDao.kt
new file mode 100644
index 0000000..c224ce5
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/dao/NoteDao.kt
@@ -0,0 +1,47 @@
+package com.dscvit.vitty.data.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import com.dscvit.vitty.data.entity.NoteEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface NoteDao {
+ @Query("SELECT * FROM notes WHERE courseId = :courseId ORDER BY createdAt DESC")
+ fun getNotesByCourse(courseId: String): Flow>
+
+ @Query("SELECT * FROM notes WHERE courseId = :courseId AND isStarred = 1 ORDER BY createdAt DESC")
+ fun getStarredNotesByCourse(courseId: String): Flow>
+
+ @Query("SELECT * FROM notes WHERE id = :noteId")
+ suspend fun getNoteById(noteId: Long): NoteEntity?
+
+ @Insert
+ suspend fun insertNote(note: NoteEntity): Long
+
+ @Update
+ suspend fun updateNote(note: NoteEntity)
+
+ @Delete
+ suspend fun deleteNote(note: NoteEntity)
+
+ @Query("DELETE FROM notes WHERE courseId = :courseId")
+ suspend fun deleteNotesByCourse(courseId: String)
+
+ @Query("UPDATE notes SET isStarred = :isStarred WHERE id = :noteId")
+ suspend fun updateStarredStatus(
+ noteId: Long,
+ isStarred: Boolean,
+ )
+
+ @Query(
+ "SELECT * FROM notes WHERE courseId = :courseId AND (title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%') ORDER BY createdAt DESC",
+ )
+ fun searchNotes(
+ courseId: String,
+ query: String,
+ ): Flow>
+}
diff --git a/app/src/main/java/com/dscvit/vitty/data/dao/ReminderDao.kt b/app/src/main/java/com/dscvit/vitty/data/dao/ReminderDao.kt
new file mode 100644
index 0000000..2a5fe0d
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/dao/ReminderDao.kt
@@ -0,0 +1,47 @@
+package com.dscvit.vitty.data.dao
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import com.dscvit.vitty.data.entity.ReminderEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface ReminderDao {
+ @Query("SELECT * FROM reminders WHERE courseId = :courseId ORDER BY dateMillis ASC")
+ fun getRemindersByCourse(courseId: String): Flow>
+
+ @Query("SELECT * FROM reminders ORDER BY dateMillis ASC")
+ fun getAllReminders(): Flow>
+
+ @Query("SELECT * FROM reminders WHERE id = :id")
+ suspend fun getReminderById(id: Long): ReminderEntity?
+
+ @Query("SELECT * FROM reminders WHERE dateMillis >= :startTime AND dateMillis <= :endTime AND isCompleted = 0")
+ suspend fun getRemindersInRange(
+ startTime: Long,
+ endTime: Long,
+ ): List
+
+ @Insert
+ suspend fun insertReminder(reminder: ReminderEntity): Long
+
+ @Update
+ suspend fun updateReminder(reminder: ReminderEntity)
+
+ @Delete
+ suspend fun deleteReminder(reminder: ReminderEntity)
+
+ @Query("DELETE FROM reminders WHERE courseId = :courseId")
+ suspend fun deleteRemindersByCourse(courseId: String)
+
+ @Query("UPDATE reminders SET isCompleted = :isCompleted WHERE id = :id")
+ suspend fun updateCompletedStatus(
+ id: Long,
+ isCompleted: Boolean,
+ )
+
+ @Query("SELECT * FROM reminders WHERE isCompleted = 0 ORDER BY dateMillis ASC")
+ suspend fun getAllPendingReminders(): List
+}
diff --git a/app/src/main/java/com/dscvit/vitty/data/database/VittyDatabase.kt b/app/src/main/java/com/dscvit/vitty/data/database/VittyDatabase.kt
new file mode 100644
index 0000000..1a5f3d2
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/database/VittyDatabase.kt
@@ -0,0 +1,43 @@
+package com.dscvit.vitty.data.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.dscvit.vitty.data.converter.Converters
+import com.dscvit.vitty.data.dao.NoteDao
+import com.dscvit.vitty.data.dao.ReminderDao
+import com.dscvit.vitty.data.entity.NoteEntity
+import com.dscvit.vitty.data.entity.ReminderEntity
+
+@Database(
+ entities = [NoteEntity::class, ReminderEntity::class],
+ version = 3,
+ exportSchema = false,
+)
+@TypeConverters(Converters::class)
+abstract class VittyDatabase : RoomDatabase() {
+ abstract fun noteDao(): NoteDao
+ abstract fun reminderDao(): ReminderDao
+
+ companion object {
+ @Volatile
+ private var _instance: VittyDatabase? = null
+
+ fun getDatabase(context: Context): VittyDatabase =
+ _instance ?: synchronized(this) {
+ val instance =
+ Room
+ .databaseBuilder(
+ context.applicationContext,
+ VittyDatabase::class.java,
+ "vitty_database",
+ )
+ .fallbackToDestructiveMigration(true)
+ .build()
+ _instance = instance
+ instance
+ }
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/data/entity/NoteEntity.kt b/app/src/main/java/com/dscvit/vitty/data/entity/NoteEntity.kt
new file mode 100644
index 0000000..aeec5f6
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/entity/NoteEntity.kt
@@ -0,0 +1,19 @@
+package com.dscvit.vitty.data.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.dscvit.vitty.ui.coursepage.models.NoteType
+
+@Entity(tableName = "notes")
+data class NoteEntity(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+ val courseId: String,
+ val title: String,
+ val content: String,
+ val type: NoteType,
+ val isStarred: Boolean = false,
+ val imagePath: String? = null,
+ val createdAt: Long = System.currentTimeMillis(),
+ val updatedAt: Long = System.currentTimeMillis(),
+)
diff --git a/app/src/main/java/com/dscvit/vitty/data/entity/ReminderEntity.kt b/app/src/main/java/com/dscvit/vitty/data/entity/ReminderEntity.kt
new file mode 100644
index 0000000..7bd249a
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/entity/ReminderEntity.kt
@@ -0,0 +1,25 @@
+package com.dscvit.vitty.data.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "reminders")
+data class ReminderEntity(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+ val courseId: String,
+ val courseTitle: String,
+ val title: String,
+ val description: String,
+ val dateMillis: Long,
+ val fromTimeHour: Int,
+ val fromTimeMinute: Int,
+ val toTimeHour: Int,
+ val toTimeMinute: Int,
+ val isAllDay: Boolean,
+ val alertDaysBefore: Int,
+ val attachmentUrl: String? = null,
+ val isCompleted: Boolean = false,
+ val createdAt: Long = System.currentTimeMillis(),
+ val updatedAt: Long = System.currentTimeMillis()
+)
diff --git a/app/src/main/java/com/dscvit/vitty/data/repository/NoteRepository.kt b/app/src/main/java/com/dscvit/vitty/data/repository/NoteRepository.kt
new file mode 100644
index 0000000..6587176
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/repository/NoteRepository.kt
@@ -0,0 +1,79 @@
+package com.dscvit.vitty.data.repository
+
+import com.dscvit.vitty.data.dao.NoteDao
+import com.dscvit.vitty.data.entity.NoteEntity
+import com.dscvit.vitty.ui.coursepage.models.Note
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class NoteRepository(
+ private val noteDao: NoteDao,
+) {
+ fun getNotesByCourse(courseId: String): Flow> =
+ noteDao.getNotesByCourse(courseId).map { entities ->
+ entities.map { it.toNote() }
+ }
+
+ suspend fun getNoteById(noteId: Long): Note? = noteDao.getNoteById(noteId)?.toNote()
+
+ suspend fun insertNote(
+ note: Note,
+ courseId: String,
+ ): Long = noteDao.insertNote(note.toEntity(courseId))
+
+ suspend fun updateNote(
+ note: Note,
+ courseId: String,
+ noteId: Long,
+ ) {
+ noteDao.updateNote(note.toEntity(courseId, noteId))
+ }
+
+ suspend fun deleteNote(
+ note: Note,
+ courseId: String,
+ noteId: Long,
+ ) {
+ noteDao.deleteNote(note.toEntity(courseId, noteId))
+ }
+
+ suspend fun updateStarredStatus(
+ noteId: Long,
+ isStarred: Boolean,
+ ) {
+ noteDao.updateStarredStatus(noteId, isStarred)
+ }
+
+ fun searchNotes(
+ courseId: String,
+ query: String,
+ ): Flow> =
+ noteDao.searchNotes(courseId, query).map { entities ->
+ entities.map { it.toNote() }
+ }
+
+ private fun NoteEntity.toNote(): Note =
+ Note(
+ id = id,
+ title = title,
+ content = content,
+ type = type,
+ isStarred = isStarred,
+ imagePath = imagePath,
+ )
+
+ private fun Note.toEntity(
+ courseId: String,
+ id: Long = 0,
+ ): NoteEntity =
+ NoteEntity(
+ id = if (id == 0L) this.id else id,
+ courseId = courseId,
+ title = title,
+ content = content,
+ type = type,
+ isStarred = isStarred,
+ imagePath = imagePath,
+ updatedAt = System.currentTimeMillis(),
+ )
+}
diff --git a/app/src/main/java/com/dscvit/vitty/data/repository/ReminderRepository.kt b/app/src/main/java/com/dscvit/vitty/data/repository/ReminderRepository.kt
new file mode 100644
index 0000000..2d6beec
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/data/repository/ReminderRepository.kt
@@ -0,0 +1,108 @@
+package com.dscvit.vitty.data.repository
+
+import com.dscvit.vitty.data.dao.ReminderDao
+import com.dscvit.vitty.data.entity.ReminderEntity
+import com.dscvit.vitty.ui.coursepage.models.Reminder
+import com.dscvit.vitty.ui.coursepage.models.ReminderStatus
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+class ReminderRepository(
+ private val reminderDao: ReminderDao,
+) {
+ fun getRemindersByCourse(courseId: String): Flow> =
+ reminderDao.getRemindersByCourse(courseId).map { entities ->
+ entities.map { it.toReminder() }
+ }
+
+ fun getAllReminders(): Flow> =
+ reminderDao.getAllReminders().map { entities ->
+ entities.map { it.toReminder() }
+ }
+
+ suspend fun insertReminder(
+ reminder: Reminder,
+ courseId: String,
+ courseTitle: String,
+ ): Long = reminderDao.insertReminder(reminder.toEntity(courseId, courseTitle))
+
+ suspend fun deleteReminder(
+ reminder: Reminder,
+ courseId: String,
+ courseTitle: String,
+ id: Long,
+ ) {
+ reminderDao.deleteReminder(reminder.toEntity(courseId, courseTitle, id))
+ }
+
+ suspend fun updateCompletedStatus(
+ id: Long,
+ isCompleted: Boolean,
+ ) {
+ reminderDao.updateCompletedStatus(id, isCompleted)
+ }
+
+ private fun ReminderEntity.toReminder(): Reminder {
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = dateMillis
+ val currentTime = System.currentTimeMillis()
+
+ val status =
+ when {
+ isCompleted -> ReminderStatus.COMPLETED
+ dateMillis < currentTime -> ReminderStatus.UPCOMING
+ else -> ReminderStatus.CAN_WAIT
+ }
+
+ val dateFormat = SimpleDateFormat("dd MMM", Locale.getDefault())
+ val fullDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+
+ return Reminder(
+ id = id,
+ title = title,
+ description = description,
+ dueDate = dateFormat.format(Date(dateMillis)),
+ date = fullDateFormat.format(Date(dateMillis)),
+ status = status,
+ dateMillis = dateMillis,
+ fromTime = if (isAllDay) "" else String.format(Locale.getDefault(), "%02d:%02d", fromTimeHour, fromTimeMinute),
+ toTime = if (isAllDay) "" else String.format(Locale.getDefault(), "%02d:%02d", toTimeHour, toTimeMinute),
+ isAllDay = isAllDay,
+ alertDaysBefore = alertDaysBefore,
+ attachmentUrl = attachmentUrl,
+ courseId = courseId,
+ courseTitle = courseTitle,
+ )
+ }
+
+ private fun Reminder.toEntity(
+ courseId: String,
+ courseTitle: String,
+ id: Long = 0,
+ ): ReminderEntity {
+ val fromTimeParts = fromTime.split(":")
+ val toTimeParts = toTime.split(":")
+
+ return ReminderEntity(
+ id = if (id == 0L) this.id else id,
+ courseId = courseId.ifEmpty { this.courseId },
+ courseTitle = courseTitle.ifEmpty { this.courseTitle },
+ title = title,
+ description = description,
+ dateMillis = dateMillis,
+ fromTimeHour = if (fromTimeParts.size >= 2) fromTimeParts[0].toIntOrNull() ?: 0 else 0,
+ fromTimeMinute = if (fromTimeParts.size >= 2) fromTimeParts[1].toIntOrNull() ?: 0 else 0,
+ toTimeHour = if (toTimeParts.size >= 2) toTimeParts[0].toIntOrNull() ?: 0 else 0,
+ toTimeMinute = if (toTimeParts.size >= 2) toTimeParts[1].toIntOrNull() ?: 0 else 0,
+ isAllDay = isAllDay,
+ alertDaysBefore = alertDaysBefore,
+ attachmentUrl = attachmentUrl,
+ isCompleted = status == ReminderStatus.COMPLETED,
+ updatedAt = System.currentTimeMillis(),
+ )
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt
new file mode 100644
index 0000000..f3a0380
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt
@@ -0,0 +1,227 @@
+package com.dscvit.vitty.network.api.community
+
+import com.dscvit.vitty.network.api.community.requests.CampusUpdateRequestBody
+import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody
+import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody
+import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse
+import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse
+import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleRequest
+import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse
+import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse
+import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse
+import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse
+import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.CircleResponse
+import com.dscvit.vitty.network.api.community.responses.user.FriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse
+import com.dscvit.vitty.network.api.community.responses.user.PostResponse
+import com.dscvit.vitty.network.api.community.responses.user.SignInResponse
+import com.dscvit.vitty.network.api.community.responses.user.UserResponse
+import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Headers
+import retrofit2.http.PATCH
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface APICommunity {
+ @GET("/")
+ fun checkServerStatus(): Call
+
+ @Headers("Content-Type: application/json")
+ @POST("/api/v3/auth/check-username")
+ fun checkUsername(
+ @Body body: UsernameRequestBody,
+ ): Call
+
+ @Headers("Content-Type: application/json")
+ @POST("/api/v3/auth/firebase/")
+ fun signInInfo(
+ @Body body: Any,
+ ): Call
+
+ @GET("/api/v3/users/{username}")
+ fun getUser(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/timetable/{username}/")
+ fun getTimeTable(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/circles/{circleId}/{username}/")
+ fun getCircleTimeTable(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/friends/{username}/")
+ fun getFriendList(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/users/search")
+ fun searchUsers(
+ @Header("Authorization") authToken: String,
+ @Query("query") query: String,
+ ): Call>
+
+ @GET("/api/v3/requests/")
+ fun getFriendRequests(
+ @Header("Authorization") authToken: String,
+ ): Call
+
+ @GET("/api/v3/users/suggested/")
+ fun getSuggestedFriends(
+ @Header("Authorization") authToken: String,
+ ): Call>
+
+ @POST("/api/v3/requests/{username}/send")
+ fun sendRequest(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @POST("/api/v3/requests/{username}/accept/")
+ fun acceptRequest(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @POST("/api/v3/requests/{username}/decline/")
+ fun declineRequest(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @DELETE("/api/v3/friends/{username}/")
+ fun deleteFriend(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @Headers("Content-Type: application/json")
+ @PATCH("/api/v3/users/campus")
+ fun updateCampus(
+ @Header("Authorization") authToken: String,
+ @Body campusRequestBody: CampusUpdateRequestBody,
+ ): Call
+
+ @POST("/api/v3/friends/ghost/{username}")
+ fun enableGhostMode(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @POST("/api/v3/friends/alive/{username}")
+ fun disableGhostMode(
+ @Header("Authorization") authToken: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/circles")
+ fun getCircles(
+ @Header("Authorization") authToken: String,
+ ): Call
+
+ @POST("/api/v3/circles/create")
+ fun createCircle(
+ @Header("Authorization") authToken: String,
+ @Body requestBody: CreateCircleRequest //
+ ): Call
+
+ @POST("/api/v3/circles/join")
+ fun joinCircleByCode(
+ @Header("Authorization") authToken: String,
+ @Query("code") joinCode: String,
+ ): Call
+
+ @GET("/api/v3/circles/{circleId}")
+ fun getCircleDetails(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ ): Call
+
+ @POST("/api/v3/circles/sendRequest/{circleId}/{username}")
+ fun sendCircleRequest(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ @Path("username") username: String,
+ ): Call
+
+ @Headers("Content-Type: application/json")
+ @POST("/api/v3/circles/sendRequest/{circleId}")
+ fun sendBatchCircleRequest(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ @Body body: CircleBatchRequestBody,
+ ): Call
+
+ @GET("/api/v3/circles/requests/received")
+ fun getReceivedCircleRequests(
+ @Header("Authorization") authToken: String,
+ ): Call
+
+ @GET("/api/v3/circles/requests/sent")
+ fun getSentCircleRequests(
+ @Header("Authorization") authToken: String,
+ ): Call
+
+ @DELETE("/api/v3/circles/{circleId}")
+ fun deleteCircle(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ ): Call
+
+ @DELETE("/api/v3/circles/leave/{circleId}")
+ fun leaveCircle(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ ): Call
+
+ @POST("/api/v3/circles/acceptRequest/{circleId}")
+ fun acceptCircleRequest(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ ): Call
+
+ @POST("/api/v3/circles/declineRequest/{circleId}")
+ fun declineCircleRequest(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ ): Call
+
+ @DELETE("/api/v3/circles/unsendRequest/{circleId}/{username}")
+ fun unsendCircleRequest(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ @Path("username") username: String,
+ ): Call
+
+ @DELETE("/api/v3/circles/remove/{circleId}/{username}")
+ fun removeUserFromCircle(
+ @Header("Authorization") authToken: String,
+ @Path("circleId") circleId: String,
+ @Path("username") username: String,
+ ): Call
+
+ @GET("/api/v3/timetable/emptyClassRooms")
+ fun getEmptyClassrooms(
+ @Header("Authorization") authToken: String,
+ @Query("slot") slot: String,
+ ): Call>>
+
+ @GET(value = "/api/v3/friends/active")
+ fun getActiveFriends(
+ @Header("Authorization") authToken: String,
+ ): Call
+}
diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt
new file mode 100644
index 0000000..5ef302a
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt
@@ -0,0 +1,1079 @@
+package com.dscvit.vitty.network.api.community
+
+import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithCampus
+import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithoutCampus
+import com.dscvit.vitty.network.api.community.requests.CampusUpdateRequestBody
+import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody
+import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody
+import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse
+import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse
+import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleRequest
+import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse
+import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse
+import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse
+import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse
+import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.CircleResponse
+import com.dscvit.vitty.network.api.community.responses.user.FriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse
+import com.dscvit.vitty.network.api.community.responses.user.PostResponse
+import com.dscvit.vitty.network.api.community.responses.user.SignInResponse
+import com.dscvit.vitty.network.api.community.responses.user.UserResponse
+import com.google.gson.Gson
+import com.google.gson.JsonSyntaxException
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import timber.log.Timber
+
+class APICommunityRestClient {
+ companion object {
+ val instance = APICommunityRestClient()
+ }
+
+ private var mApiUser: APICommunity? = null
+ private val retrofit = CommunityNetworkClient.retrofitClientCommunity
+
+ fun signInWithUsernameRegNo(
+ username: String,
+ regno: String,
+ uuid: String,
+ campus: String,
+ retrofitCommunitySignInListener: RetrofitCommunitySignInListener,
+ retrofitSelfUserListener: RetrofitSelfUserListener,
+ ) {
+ mApiUser = retrofit.create(APICommunity::class.java)
+
+ val requestBody =
+ if (campus != "") {
+ AuthRequestBodyWithCampus(
+ reg_no = regno,
+ username = username,
+ uuid = uuid,
+ campus = campus,
+ )
+ } else {
+ AuthRequestBodyWithoutCampus(
+ reg_no = regno,
+ username = username,
+ uuid = uuid,
+ )
+ }
+
+ val apiSignInCall = mApiUser!!.signInInfo(requestBody)
+
+ apiSignInCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitCommunitySignInListener.onSuccess(call, response.body())
+ val token = response.body()?.token.toString()
+ val res_username = response.body()?.username.toString()
+
+ getUserWithTimeTable(token, res_username, retrofitSelfUserListener)
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitCommunitySignInListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getUserWithTimeTable(
+ token: String,
+ username: String,
+ retrofitSelfUserListener: RetrofitSelfUserListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiUserCall = mApiUser!!.getUser(bearerToken, username)
+ apiUserCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("UserResponse: $response")
+ retrofitSelfUserListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.e("Error fetching user with timetable: ${t.message}")
+ retrofitSelfUserListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getTimeTable(
+ token: String,
+ username: String,
+ retrofitTimeTableListener: RetrofitTimetableListener,
+ ) {
+ val token = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiTimetableCall = mApiUser!!.getTimeTable(token, username)
+ apiTimetableCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitTimeTableListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitTimeTableListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getCircleTimeTable(
+ token: String,
+ circleId: String,
+ username: String,
+ retrofitTimeTableListener: RetrofitTimetableListener,
+ ) {
+ val token = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiCircleTimetableCall = mApiUser!!.getCircleTimeTable(token, circleId, username)
+ apiCircleTimetableCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitTimeTableListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitTimeTableListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getFriendList(
+ token: String,
+ username: String,
+ retrofitFriendListListener: RetrofitFriendListListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiFriendListCall = mApiUser!!.getFriendList(bearerToken, username)
+ apiFriendListCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("FriendListResponse: ${response.body()}")
+ retrofitFriendListListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitFriendListListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getCircles(
+ token: String,
+ retrofitCircleListener: RetrofitCircleListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiCirclesCall = mApiUser!!.getCircles(bearerToken)
+ apiCirclesCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitCircleListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitCircleListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun createCircle(
+ token: String,
+ circleName: String,
+ retrofitCreateCircleListener: RetrofitCreateCircleListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ Timber.d("APICommunityRestClient.createCircle called with circleName: $circleName")
+ val requestBody = CreateCircleRequest(circleName = circleName)
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiCreateCircleCall = mApiUser!!.createCircle(bearerToken, requestBody)
+ Timber.d("API call created with request body, enqueueing request...")
+
+
+ apiCreateCircleCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("API Response received: ${response.code()}, body: ${response.body()}")
+ if (response.isSuccessful) {
+ retrofitCreateCircleListener.onSuccess(call, response.body())
+ } else {
+ val errorBody = response.errorBody()?.string()
+ Timber.e("API Error: ${response.code()} - $errorBody")
+ retrofitCreateCircleListener.onError(call, Throwable("API Error: ${response.code()} - ${errorBody ?: "Unknown error"}"))
+ }
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.e("API Call failed: ${t.message}")
+ retrofitCreateCircleListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun joinCircleByCode(
+ token: String,
+ joinCode: String,
+ retrofitJoinCircleListener: RetrofitJoinCircleListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ Timber.d("APICommunityRestClient.joinCircleByCode called with joinCode: $joinCode")
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiJoinCircleCall = mApiUser!!.joinCircleByCode(bearerToken, joinCode)
+
+ Timber.d("Join circle API call created, enqueueing request...")
+
+ apiJoinCircleCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("Join Circle API Response received: ${response.code()}, body: ${response.body()}")
+ if (response.isSuccessful) {
+ retrofitJoinCircleListener.onSuccess(call, response.body())
+ } else {
+ // Handle error responses (400, 409, 500)
+ val errorMessage =
+ when (response.code()) {
+ 400 -> "Invalid join code"
+ 409 -> "You are already part of the circle"
+ 500 -> "Failed to join circle"
+ else -> "Unknown error occurred"
+ }
+ Timber.e("Join Circle API Error: ${response.code()} - $errorMessage")
+ retrofitJoinCircleListener.onError(call, Throwable(errorMessage))
+ }
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.e("Join Circle API Call failed: ${t.message}")
+ retrofitJoinCircleListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getCircleDetails(
+ token: String,
+ circleId: String,
+ retrofitFriendListListener: RetrofitFriendListListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiCircleDetailsCall = mApiUser!!.getCircleDetails(bearerToken, circleId)
+ apiCircleDetailsCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitFriendListListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitFriendListListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getSearchResult(
+ token: String,
+ query: String,
+ retrofitSearchResultListener: RetrofitSearchResultListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiSearchResultCall = mApiUser!!.searchUsers(bearerToken, query)
+ apiSearchResultCall.enqueue(
+ object : Callback> {
+ override fun onResponse(
+ call: Call>,
+ response: Response>,
+ ) {
+ Timber.d("SearchResult4: $response")
+ retrofitSearchResultListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call>,
+ t: Throwable,
+ ) {
+ retrofitSearchResultListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getSuggestedFriends(
+ token: String,
+ retrofitSearchResultListener: RetrofitSearchResultListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiSuggestedResultCall = mApiUser!!.getSuggestedFriends(bearerToken)
+ apiSuggestedResultCall.enqueue(
+ object : Callback> {
+ override fun onResponse(
+ call: Call>,
+ response: Response>,
+ ) {
+ Timber.d("SearchResult4: $response")
+ Timber.d("Response Code: ${response.code()}")
+ Timber.d("Response Headers: ${response.headers()}")
+ Timber.d("Response Body: ${response.body()}")
+ Timber.d("Response Raw: ${response.raw()}")
+ Timber.d("Response isSuccessful: ${response.isSuccessful}")
+
+ if (response.isSuccessful) {
+ val body = response.body()
+ if (body != null) {
+ Timber.d("Suggested Friends Count: ${body.size}")
+ retrofitSearchResultListener.onSuccess(call, body)
+ } else {
+ Timber.d("Response body is null, treating as empty list")
+ retrofitSearchResultListener.onSuccess(call, emptyList())
+ }
+ } else {
+ Timber.d("Response not successful: ${response.code()}")
+ retrofitSearchResultListener.onError(call, Exception("HTTP ${response.code()}"))
+ }
+ }
+
+ override fun onFailure(
+ call: Call>,
+ t: Throwable,
+ ) {
+ Timber.d("API Call Failed: ${t.message}")
+ retrofitSearchResultListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getFriendRequest(
+ token: String,
+ retrofitFriendRequestListener: RetrofitFriendRequestListener,
+ ) {
+ val bearerToken = "Bearer $token"
+ Timber.d("FriendReqToken--: $bearerToken")
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiFriendRequestCall = mApiUser!!.getFriendRequests(bearerToken)
+ apiFriendRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("FriendRequest--: $response")
+ retrofitFriendRequestListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("FriendRequestError--: ${t.message}")
+ retrofitFriendRequestListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun acceptRequest(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiAcceptRequestCall = mApiUser!!.acceptRequest(bearerToken, username)
+ apiAcceptRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun rejectRequest(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiRejectRequestCall = mApiUser!!.declineRequest(bearerToken, username)
+ apiRejectRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun sendRequest(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiSendRequestCall = mApiUser!!.sendRequest(bearerToken, username)
+ apiSendRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun unfriend(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiUnfriendCall = mApiUser!!.deleteFriend(bearerToken, username)
+ apiUnfriendCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun sendCircleRequest(
+ token: String,
+ circleId: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiSendCircleRequestCall = mApiUser!!.sendCircleRequest(bearerToken, circleId, username)
+ apiSendCircleRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("SendCircleResponse: $response")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun sendBatchCircleRequest(
+ token: String,
+ circleId: String,
+ usernames: List,
+ callback: (CircleBatchRequestResponse?) -> Unit,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val requestBody = CircleBatchRequestBody(usernames)
+ val apiSendBatchCircleRequestCall = mApiUser!!.sendBatchCircleRequest(bearerToken, circleId, requestBody)
+ apiSendBatchCircleRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("SendBatchCircleResponse: $response")
+ callback(response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("SendBatchCircleRequestError: ${t.message}")
+ callback(null)
+ }
+ },
+ )
+ }
+
+ fun checkUsername(
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val usernameRequestBody = UsernameRequestBody(username)
+ val apiCheckUsernameCall = mApiUser!!.checkUsername(usernameRequestBody)
+ apiCheckUsernameCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ if (response.isSuccessful) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ } else {
+ val gson = Gson()
+ val errorString = response.errorBody()?.string()
+ Timber.d("ResponseV: $errorString")
+ try {
+ val errorResponse = gson.fromJson(errorString, PostResponse::class.java)
+ retrofitUserActionListener.onSuccess(call, errorResponse)
+ } catch (e: JsonSyntaxException) {
+ // Handle any JSON parsing errors.
+ val errorMessage = "Username is not valid/available."
+ retrofitUserActionListener.onSuccess(call, PostResponse(errorMessage))
+ }
+ }
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("ErrorV: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun enableGhostMode(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitGhostActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiGhostModeCall = mApiUser!!.enableGhostMode(bearerToken, username)
+ apiGhostModeCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun disableGhostMode(
+ token: String,
+ username: String,
+ retrofitUserActionListener: RetrofitGhostActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiGhostModeCall = mApiUser!!.disableGhostMode(bearerToken, username)
+ apiGhostModeCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getReceivedCircleRequests(
+ token: String,
+ retrofitCircleRequestListener: RetrofitCircleRequestListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiReceivedCircleRequestsCall = mApiUser!!.getReceivedCircleRequests(bearerToken)
+ apiReceivedCircleRequestsCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("ReceivedCircleRequests: ${response.body()}")
+ retrofitCircleRequestListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("ReceivedCircleRequestsError: ${t.message}")
+ retrofitCircleRequestListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getSentCircleRequests(
+ token: String,
+ retrofitCircleRequestListener: RetrofitCircleRequestListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiSentCircleRequestsCall = mApiUser!!.getSentCircleRequests(bearerToken)
+ apiSentCircleRequestsCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("SentCircleRequests: ${response.body()}")
+ retrofitCircleRequestListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("SentCircleRequestsError: ${t.message}")
+ retrofitCircleRequestListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun deleteCircle(
+ token: String,
+ circleId: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiDeleteCircleCall = mApiUser!!.deleteCircle(bearerToken, circleId)
+ apiDeleteCircleCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("DeleteCircle: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("DeleteCircleError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun leaveCircle(
+ token: String,
+ circleId: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiLeaveCircleCall = mApiUser!!.leaveCircle(bearerToken, circleId)
+ apiLeaveCircleCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("LeaveCircle: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("LeaveCircleError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun acceptCircleRequest(
+ token: String,
+ circleId: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiAcceptCircleRequestCall = mApiUser!!.acceptCircleRequest(bearerToken, circleId)
+ apiAcceptCircleRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("AcceptCircleRequest: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("AcceptCircleRequestError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun declineCircleRequest(
+ token: String,
+ circleId: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiDeclineCircleRequestCall = mApiUser!!.declineCircleRequest(bearerToken, circleId)
+ apiDeclineCircleRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("DeclineCircleRequest: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("DeclineCircleRequestError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun unsendCircleRequest(
+ token: String,
+ circleId: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiUnsendCircleRequestCall = mApiUser!!.unsendCircleRequest(bearerToken, circleId, username)
+ apiUnsendCircleRequestCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("UnsendCircleRequest: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("UnsendCircleRequestError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getEmptyClassrooms(
+ token: String,
+ slot: String,
+ callback: (Map>?) -> Unit,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiEmptyClassroomsCall = mApiUser!!.getEmptyClassrooms(bearerToken, slot)
+ apiEmptyClassroomsCall.enqueue(
+ object : Callback>> {
+ override fun onResponse(
+ call: Call>>,
+ response: Response>>,
+ ) {
+ Timber.d("EmptyClassrooms: ${response.body()}")
+ callback(response.body())
+ }
+
+ override fun onFailure(
+ call: Call>>,
+ t: Throwable,
+ ) {
+ Timber.d("EmptyClassroomsError: ${t.message}")
+ callback(null)
+ }
+ },
+ )
+ }
+
+ fun removeUserFromCircle(
+ token: String,
+ circleId: String,
+ username: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiRemoveUserCall = mApiUser!!.removeUserFromCircle(bearerToken, circleId, username)
+ apiRemoveUserCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("RemoveUserFromCircle: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("RemoveUserFromCircleError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun updateCampus(
+ token: String,
+ campus: String,
+ retrofitUserActionListener: RetrofitUserActionListener,
+ ) {
+ val bearerToken = "Bearer $token"
+
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val requestBody = CampusUpdateRequestBody(campus)
+ val apiUpdateCampusCall = mApiUser!!.updateCampus(bearerToken, requestBody)
+ apiUpdateCampusCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("UpdateCampus: ${response.body()}")
+ retrofitUserActionListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("UpdateCampusError: ${t.message}")
+ retrofitUserActionListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun getActiveFriends(
+ token: String,
+ retrofitActiveFriendsListener: RetrofitActiveFriendsListener,
+ ) {
+ val bearerToken = "Bearer $token"
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiActiveFriendsCall = mApiUser!!.getActiveFriends(bearerToken)
+ apiActiveFriendsCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("ActiveFriends: ${response.body()}")
+ retrofitActiveFriendsListener.onSuccess(call, response.body())
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("ActiveFriendsError: ${t.message}")
+ retrofitActiveFriendsListener.onError(call, t)
+ }
+ },
+ )
+ }
+
+ fun checkServerStatus(
+ retrofitServerStatusListener: RetrofitServerStatusListener,
+ ) {
+ mApiUser = retrofit.create(APICommunity::class.java)
+ val apiServerStatusCall = mApiUser!!.checkServerStatus()
+ apiServerStatusCall.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("ServerStatus: ${response.body()}")
+ retrofitServerStatusListener.onSuccess(call, response.body(), response.isSuccessful)
+ }
+
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ Timber.d("ServerStatusError: ${t.message}")
+ retrofitServerStatusListener.onError(call, t)
+ }
+ },
+ )
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt
new file mode 100644
index 0000000..c9b4f93
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt
@@ -0,0 +1,41 @@
+package com.dscvit.vitty.network.api.community
+
+import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL
+import com.dscvit.vitty.util.WebConstants.TIMEOUT
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+object CommunityNetworkClient {
+ private var retrofit: Retrofit? = null
+
+ val retrofitClientCommunity: Retrofit
+ get() {
+ if (retrofit == null) {
+ val loggingInterceptor =
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+ val okHttpClient =
+ OkHttpClient
+ .Builder()
+ .addInterceptor(loggingInterceptor)
+ .connectTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS)
+ .readTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS)
+ .writeTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS)
+ .build()
+
+ retrofit =
+ Retrofit
+ .Builder()
+ .baseUrl(COMMUNITY_BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(okHttpClient)
+ .build()
+ }
+ return retrofit!!
+ }
+}
diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt
new file mode 100644
index 0000000..f178876
--- /dev/null
+++ b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt
@@ -0,0 +1,183 @@
+package com.dscvit.vitty.network.api.community
+
+import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse
+import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse
+import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse
+import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse
+import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse
+import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.CircleResponse
+import com.dscvit.vitty.network.api.community.responses.user.FriendResponse
+import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse
+import com.dscvit.vitty.network.api.community.responses.user.PostResponse
+import com.dscvit.vitty.network.api.community.responses.user.SignInResponse
+import com.dscvit.vitty.network.api.community.responses.user.UserResponse
+import retrofit2.Call
+
+interface RetrofitCommunitySignInListener {
+ fun onSuccess(
+ call: Call?,
+ response: SignInResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitSelfUserListener {
+ fun onSuccess(
+ call: Call?,
+ response: UserResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitTimetableListener {
+ fun onSuccess(
+ call: Call?,
+ response: TimetableResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitFriendListListener {
+ fun onSuccess(
+ call: Call?,
+ response: FriendResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitSearchResultListener {
+ fun onSuccess(
+ call: Call>?,
+ response: List?,
+ )
+
+ fun onError(
+ call: Call>?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitFriendRequestListener {
+ fun onSuccess(
+ call: Call?,
+ response: RequestsResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitCircleRequestListener {
+ fun onSuccess(
+ call: Call?,
+ response: CircleRequestsResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitUserActionListener {
+ fun onSuccess(
+ call: Call?,
+ response: PostResponse?,
+ )
+
+ fun onError(
+ call: Call?,
+ t: Throwable?,
+ )
+}
+
+interface RetrofitGhostActionListener {
+ fun onSuccess(
+ call: Call