From 351cb2276cbcf12c18e35dca71a1762b97245bf0 Mon Sep 17 00:00:00 2001 From: Erik Everardo Date: Mon, 6 Feb 2023 13:44:27 -0600 Subject: [PATCH] first commit --- .idea/.gitignore | 3 + .idea/.name | 1 + .idea/codeStyles/Project.xml | 123 +++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/compiler.xml | 6 + .idea/gradle.xml | 19 ++ .idea/misc.xml | 10 ++ .idea/vcs.xml | 6 + app/build.gradle | 84 +++++++++ .../com/isolaatti/ExampleInstrumentedTest.kt | 24 +++ app/src/main/AndroidManifest.xml | 29 +++ .../main/java/com/isolaatti/MainActivity.kt | 34 ++++ app/src/main/java/com/isolaatti/MainModule.kt | 27 +++ .../main/java/com/isolaatti/MyApplication.kt | 17 ++ .../isolaatti/auth/data/AuthRepositoryImpl.kt | 46 +++++ .../isolaatti/auth/data/local/TokenStorage.kt | 85 +++++++++ .../com/isolaatti/auth/data/remote/AuthApi.kt | 18 ++ .../auth/data/remote/AuthTokenDto.kt | 7 + .../data/remote/AuthTokenVerificationDto.kt | 3 + .../isolaatti/auth/data/remote/Credential.kt | 12 ++ .../isolaatti/auth/domain/AuthRepository.kt | 10 ++ .../isolaatti/connectivity/RetrofitClient.kt | 54 ++++++ .../java/com/isolaatti/home/HomeActivity.kt | 30 ++++ .../home/feed/presentation/FeedViewModel.kt | 7 + .../isolaatti/home/feed/ui/FeedFragment.kt | 39 ++++ .../presentation/NotificationsViewModel.kt | 7 + .../notifications/ui/NotificationsFragment.kt | 33 ++++ .../java/com/isolaatti/login/LogInActivity.kt | 54 ++++++ .../com/isolaatti/login/LogInViewModel.kt | 51 ++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 ++++ app/src/main/res/drawable/baseline_add_24.xml | 5 + .../main/res/drawable/baseline_home_24.xml | 5 + .../main/res/drawable/baseline_menu_24.xml | 5 + .../drawable/baseline_notifications_24.xml | 5 + .../main/res/drawable/baseline_people_24.xml | 5 + .../main/res/drawable/baseline_person_24.xml | 5 + .../res/drawable/baseline_settings_24.xml | 5 + .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ app/src/main/res/layout/activity_home.xml | 33 ++++ app/src/main/res/layout/activity_login.xml | 92 ++++++++++ app/src/main/res/layout/fragment_feed.xml | 57 ++++++ .../res/layout/fragment_notifications.xml | 21 +++ .../main/res/layout/header_drawer_home.xml | 15 ++ app/src/main/res/menu/home_drawer_menu.xml | 13 ++ app/src/main/res/menu/home_menu.xml | 12 ++ app/src/main/res/menu/home_topbar_menu.xml | 9 + app/src/main/res/menu/notifications_menu.xml | 6 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-anydpi-v33/ic_launcher.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../main/res/navigation/home_navigation.xml | 18 ++ app/src/main/res/values/colors.xml | 10 ++ app/src/main/res/values/strings.xml | 10 ++ app/src/main/res/values/themes.xml | 11 ++ app/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../java/com/isolaatti/ExampleUnitTest.kt | 17 ++ build.gradle | 7 + gradle/wrapper/gradle-wrapper.properties | 6 + settings.gradle | 16 ++ 70 files changed, 1480 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 app/build.gradle create mode 100644 app/src/androidTest/java/com/isolaatti/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/isolaatti/MainActivity.kt create mode 100644 app/src/main/java/com/isolaatti/MainModule.kt create mode 100644 app/src/main/java/com/isolaatti/MyApplication.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenVerificationDto.kt create mode 100644 app/src/main/java/com/isolaatti/auth/data/remote/Credential.kt create mode 100644 app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt create mode 100644 app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt create mode 100644 app/src/main/java/com/isolaatti/home/HomeActivity.kt create mode 100644 app/src/main/java/com/isolaatti/home/feed/presentation/FeedViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/home/feed/ui/FeedFragment.kt create mode 100644 app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt create mode 100644 app/src/main/java/com/isolaatti/login/LogInActivity.kt create mode 100644 app/src/main/java/com/isolaatti/login/LogInViewModel.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/baseline_add_24.xml create mode 100644 app/src/main/res/drawable/baseline_home_24.xml create mode 100644 app/src/main/res/drawable/baseline_menu_24.xml create mode 100644 app/src/main/res/drawable/baseline_notifications_24.xml create mode 100644 app/src/main/res/drawable/baseline_people_24.xml create mode 100644 app/src/main/res/drawable/baseline_person_24.xml create mode 100644 app/src/main/res/drawable/baseline_settings_24.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_home.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/fragment_feed.xml create mode 100644 app/src/main/res/layout/fragment_notifications.xml create mode 100644 app/src/main/res/layout/header_drawer_home.xml create mode 100644 app/src/main/res/menu/home_drawer_menu.xml create mode 100644 app/src/main/res/menu/home_menu.xml create mode 100644 app/src/main/res/menu/home_topbar_menu.xml create mode 100644 app/src/main/res/menu/notifications_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/navigation/home_navigation.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/isolaatti/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 settings.gradle diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..65d9b9b --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Isolaatti \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..a2d7c21 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..54d5acd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e27dfc8 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,84 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0' +} + +android { + namespace 'com.isolaatti' + compileSdk 33 + viewBinding { + enabled = true + } + + defaultConfig { + applicationId "com.isolaatti" + minSdk 23 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.squareup.retrofit2:converter-gson:2.3.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + // Hilt + implementation "com.google.dagger:hilt-android:2.44" + kapt "com.google.dagger:hilt-compiler:2.44" + + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + + + // Material 3 + implementation "com.google.android.material:material:1.9.0-alpha01" + + // Navigation + def nav_version = "2.5.3" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + // Splash screen + implementation "androidx.core:core-splashscreen:1.0.0" + + // Data security + implementation "androidx.security:security-crypto:1.0.0" +} + +kapt { + correctErrorTypes true +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/isolaatti/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/isolaatti/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a45a076 --- /dev/null +++ b/app/src/androidTest/java/com/isolaatti/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.isolaatti + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.isolaatti", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d55a12b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/MainActivity.kt b/app/src/main/java/com/isolaatti/MainActivity.kt new file mode 100644 index 0000000..e753492 --- /dev/null +++ b/app/src/main/java/com/isolaatti/MainActivity.kt @@ -0,0 +1,34 @@ +package com.isolaatti + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.isolaatti.auth.data.AuthRepositoryImpl +import com.isolaatti.home.HomeActivity +import com.isolaatti.login.LogInActivity +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + @Inject + lateinit var authRepository: AuthRepositoryImpl + + override fun onCreate(savedInstanceState: Bundle?) { + var isLoading = true + val splashScreen = installSplashScreen() + splashScreen.setKeepOnScreenCondition { isLoading } + super.onCreate(savedInstanceState) + // Decide what activity to start + // Set isLoading to false when ended + val currentToken = authRepository.getCurrentToken() + + if(currentToken == null) { + startActivity(Intent(this@MainActivity, LogInActivity::class.java)) + } else { + startActivity(Intent(this@MainActivity, HomeActivity::class.java)) + } + isLoading = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/MainModule.kt b/app/src/main/java/com/isolaatti/MainModule.kt new file mode 100644 index 0000000..588753b --- /dev/null +++ b/app/src/main/java/com/isolaatti/MainModule.kt @@ -0,0 +1,27 @@ +package com.isolaatti + +import com.isolaatti.auth.data.AuthRepositoryImpl +import com.isolaatti.auth.data.local.TokenStorage +import com.isolaatti.auth.data.remote.AuthApi +import com.isolaatti.auth.domain.AuthRepository +import com.isolaatti.connectivity.RetrofitClient +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class MainModule { + + @Provides + fun provideAuthApi():AuthApi { + return RetrofitClient.client.create(AuthApi::class.java) + } + + @Provides + fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi): AuthRepository { + return AuthRepositoryImpl(tokenStorage, authApi) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/MyApplication.kt b/app/src/main/java/com/isolaatti/MyApplication.kt new file mode 100644 index 0000000..469563a --- /dev/null +++ b/app/src/main/java/com/isolaatti/MyApplication.kt @@ -0,0 +1,17 @@ +package com.isolaatti + +import android.app.Application +import com.isolaatti.auth.data.AuthRepositoryImpl +import com.isolaatti.auth.data.local.TokenStorage +import com.isolaatti.auth.data.remote.AuthApi +import com.isolaatti.auth.domain.AuthRepository +import dagger.Provides +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Singleton + +@HiltAndroidApp +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt new file mode 100644 index 0000000..1c04631 --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt @@ -0,0 +1,46 @@ +package com.isolaatti.auth.data + +import android.util.Log +import com.isolaatti.auth.data.remote.AuthTokenDto +import com.isolaatti.auth.data.local.TokenStorage +import com.isolaatti.auth.data.remote.AuthApi +import com.isolaatti.auth.data.remote.Credential +import com.isolaatti.auth.domain.AuthRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import retrofit2.awaitResponse +import javax.inject.Inject + + +class AuthRepositoryImpl @Inject constructor( + private val tokenStorage: TokenStorage, + private val authApi: AuthApi +) : AuthRepository { + override fun authWithEmailAndPassword(email: String, password: String): Flow = flow { + val res = authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse() + val authDto = res.body() + + + if(authDto != null) { + tokenStorage.storeToken(authDto) + emit(true) + } else { + emit(false) + } + } + + override fun logout(): Flow = flow { + tokenStorage.removeToken() + authApi.signOut() + emit(true) + } + + override fun getCurrentToken(): AuthTokenDto? { + return tokenStorage.token + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt b/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt new file mode 100644 index 0000000..9d77841 --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt @@ -0,0 +1,85 @@ +package com.isolaatti.auth.data.local + +import android.content.Context +import androidx.security.crypto.EncryptedFile +import androidx.security.crypto.MasterKeys +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.isolaatti.auth.data.remote.AuthTokenDto +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.IOException +import java.security.GeneralSecurityException +import javax.inject.Inject + + +class TokenStorage @Inject constructor(@ApplicationContext private val application: Context) { + var token: AuthTokenDto? = null + + private val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC + private val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) + private val fileName = "token.isolaatti" + + private val file = File(application.filesDir, fileName) + private val encryptedFile = EncryptedFile.Builder( + file, + application, + mainKeyAlias, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + ).build() + + + init { + token = getSavedToken() + } + + private val gson get() = Gson() + + private fun getSavedToken(): AuthTokenDto? { + try { + + + val jsonEncodedInputStream = encryptedFile.openFileInput() + val jsonEncodedBytes = jsonEncodedInputStream.readBytes() + jsonEncodedInputStream.close() + return gson.fromJson(String(jsonEncodedBytes), AuthTokenDto::class.java) + } catch(e: IllegalArgumentException) { + return null + } catch(e: JsonSyntaxException) { + return null + } catch (e: IOException) { + return null + } catch(e: GeneralSecurityException) { + return null + } + } + + fun storeToken(token: AuthTokenDto): Boolean { + try { + val jsonEncodedBytes = gson.toJson(token).toByteArray(Charsets.UTF_8) + + if(file.exists()) { + file.delete() + } + + encryptedFile.openFileOutput().apply { + write(jsonEncodedBytes) + flush() + close() + } + + // Keeps this token in memory + this.token = token + + return true + } catch(e: IOException) { + return false + } + } + + fun removeToken() { + if(file.exists()) { + file.delete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt b/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt new file mode 100644 index 0000000..f8bef8f --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt @@ -0,0 +1,18 @@ +package com.isolaatti.auth.data.remote + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface AuthApi { + @POST("LogIn/Verify") + fun validateTokenUrl(@Header("sessionToken") sessionToken: String): Call + + @POST("LogIn") + fun signInWithEmailAndPassword(@Body credential: Credential): Call + + @GET("LogIn/SignOut") + fun signOut(): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt new file mode 100644 index 0000000..e979f6d --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt @@ -0,0 +1,7 @@ +package com.isolaatti.auth.data.remote + +data class AuthTokenDto(val expires: String, val created: String, val token: String) { + override fun toString(): String { + return token + } +} diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenVerificationDto.kt b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenVerificationDto.kt new file mode 100644 index 0000000..368c1d2 --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenVerificationDto.kt @@ -0,0 +1,3 @@ +package com.isolaatti.auth.data.remote + +data class AuthTokenVerificationDto(val isValid: Boolean, val userId: Int) diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/Credential.kt b/app/src/main/java/com/isolaatti/auth/data/remote/Credential.kt new file mode 100644 index 0000000..b889cf6 --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/data/remote/Credential.kt @@ -0,0 +1,12 @@ +package com.isolaatti.auth.data.remote + +import com.google.gson.Gson + +data class Credential( + val email: String, + val password: String +) { + override fun toString(): String { + return Gson().toJson(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt b/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt new file mode 100644 index 0000000..c8b521b --- /dev/null +++ b/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt @@ -0,0 +1,10 @@ +package com.isolaatti.auth.domain + +import com.isolaatti.auth.data.remote.AuthTokenDto +import kotlinx.coroutines.flow.Flow + +interface AuthRepository { + fun authWithEmailAndPassword(email: String, password: String): Flow + fun logout(): Flow + fun getCurrentToken(): AuthTokenDto? +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt new file mode 100644 index 0000000..ff23864 --- /dev/null +++ b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt @@ -0,0 +1,54 @@ +package com.isolaatti.connectivity + +import android.util.Log +import com.isolaatti.auth.domain.AuthRepository +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Inject +import javax.inject.Provider + +object RetrofitClient { + + // These urls don't need auth header + private val excludedUrlsFromAuthentication = listOf( + "/api/LogIn" + ) + + class AuthenticationInterceptor : Interceptor { + + @Inject lateinit var authRepository: Provider + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url() + val path = url.url().path + if(excludedUrlsFromAuthentication.contains(path)){ + return chain.proceed(chain.request()) + } + + // Add auth header here + val tokenDto = authRepository.get().getCurrentToken() + tokenDto?.token?.let { + val request = chain.request().newBuilder().addHeader("sessionToken", it) .build() + return chain.proceed(request) + } + + return chain.proceed(chain.request()) + } + + } + + + private val okHttpClient get() = OkHttpClient.Builder() + .addInterceptor(AuthenticationInterceptor()) + .build() + + + val client: Retrofit get() = Retrofit.Builder() + .baseUrl("https://isolaatti.com/api/") + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() +} diff --git a/app/src/main/java/com/isolaatti/home/HomeActivity.kt b/app/src/main/java/com/isolaatti/home/HomeActivity.kt new file mode 100644 index 0000000..def8a28 --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/HomeActivity.kt @@ -0,0 +1,30 @@ +package com.isolaatti.home + +import android.os.Bundle +import android.view.Menu +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.ActivityCompat +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.search.SearchBar +import com.isolaatti.R +import com.isolaatti.databinding.ActivityHomeBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HomeActivity : AppCompatActivity() { + lateinit var viewBinding: ActivityHomeBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewBinding = ActivityHomeBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + return super.onCreateOptionsMenu(menu) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/feed/presentation/FeedViewModel.kt b/app/src/main/java/com/isolaatti/home/feed/presentation/FeedViewModel.kt new file mode 100644 index 0000000..b3f0164 --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/feed/presentation/FeedViewModel.kt @@ -0,0 +1,7 @@ +package com.isolaatti.home.feed.presentation + +import androidx.lifecycle.ViewModel + +class FeedViewModel : ViewModel() { + // TODO: Implement the ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/feed/ui/FeedFragment.kt b/app/src/main/java/com/isolaatti/home/feed/ui/FeedFragment.kt new file mode 100644 index 0000000..30b91f9 --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/feed/ui/FeedFragment.kt @@ -0,0 +1,39 @@ +package com.isolaatti.home.feed.ui + +import androidx.lifecycle.ViewModelProvider +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.isolaatti.R +import com.isolaatti.databinding.FragmentFeedBinding +import com.isolaatti.home.feed.presentation.FeedViewModel + +class FeedFragment : Fragment() { + + companion object { + fun newInstance() = FeedFragment() + } + + private lateinit var viewModel: FeedViewModel + private lateinit var viewBinding: FragmentFeedBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + viewBinding = FragmentFeedBinding.inflate(inflater) + + viewBinding.searchBar.setNavigationOnClickListener { + viewBinding.drawerLayout.openDrawer(viewBinding.homeDrawer) + } + return viewBinding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProvider(this).get(FeedViewModel::class.java) + // TODO: Use the ViewModel + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt b/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt new file mode 100644 index 0000000..8e52cab --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt @@ -0,0 +1,7 @@ +package com.isolaatti.home.notifications.presentation + +import androidx.lifecycle.ViewModel + +class NotificationsViewModel : ViewModel() { + // TODO: Implement the ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt b/app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt new file mode 100644 index 0000000..8e7af2e --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt @@ -0,0 +1,33 @@ +package com.isolaatti.home.notifications.ui + +import androidx.lifecycle.ViewModelProvider +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.isolaatti.R +import com.isolaatti.home.notifications.presentation.NotificationsViewModel + +class NotificationsFragment : Fragment() { + + companion object { + fun newInstance() = NotificationsFragment() + } + + private lateinit var viewModel: NotificationsViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_notifications, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProvider(this).get(NotificationsViewModel::class.java) + // TODO: Use the ViewModel + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/login/LogInActivity.kt b/app/src/main/java/com/isolaatti/login/LogInActivity.kt new file mode 100644 index 0000000..e77b748 --- /dev/null +++ b/app/src/main/java/com/isolaatti/login/LogInActivity.kt @@ -0,0 +1,54 @@ +package com.isolaatti.login + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.ViewModelProvider +import com.isolaatti.databinding.ActivityLoginBinding +import com.isolaatti.home.HomeActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class LogInActivity: AppCompatActivity() { + lateinit var viewBinding: ActivityLoginBinding + val viewModel: LogInViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewBinding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + + viewModel.signInSuccess.observe(this) {success -> + if(success) + startActivity(Intent(this, HomeActivity::class.java)) + else + Toast.makeText(this,"Could not sign in, your credential is not correct...", Toast.LENGTH_SHORT).show() + // Show login error message. + } + + viewModel.formIsValid.observe(this) {isValid -> + viewBinding.signInBtn.isEnabled = isValid + } + + viewBinding.textFieldEmail.editText?.doOnTextChanged { text, start, before, count -> + // Email Validation + viewModel.validateEmail(text.toString()) + } + + viewBinding.textFieldPassword.editText?.doOnTextChanged { text, start, before, count -> + // Password validation + viewModel.validatePassword(text.toString()) + } + + viewBinding.signInBtn.setOnClickListener { + val email = viewBinding.textFieldEmail.editText?.text + val password = viewBinding.textFieldPassword.editText?.text + viewModel.signIn(email.toString(), password.toString()) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/login/LogInViewModel.kt b/app/src/main/java/com/isolaatti/login/LogInViewModel.kt new file mode 100644 index 0000000..600399a --- /dev/null +++ b/app/src/main/java/com/isolaatti/login/LogInViewModel.kt @@ -0,0 +1,51 @@ +package com.isolaatti.login + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.isolaatti.auth.domain.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() { + val signInSuccess: MutableLiveData = MutableLiveData(false) + val formIsValid: MutableLiveData = MutableLiveData(false) + val emailUserInputIsValid: MutableLiveData = MutableLiveData(false) + val passwordUserInputIsValid: MutableLiveData = MutableLiveData(false) + + fun validateEmail(email: String) { + // TODO Use regular expression + val valid = email.isNotEmpty() + emailUserInputIsValid.postValue(valid) + formIsValid.postValue(valid && passwordUserInputIsValid.value == true) + } + + fun validatePassword(password: String) { + val valid = password.isNotEmpty() + passwordUserInputIsValid.postValue(valid) + formIsValid.postValue(valid && emailUserInputIsValid.value == true) + } + + fun signIn(email: String, password: String) { + viewModelScope.launch { + authRepository.authWithEmailAndPassword(email, password).collect { + Log.d("login", it.toString()) + signInSuccess.postValue(it) + } + } + } + + fun signUp() { + + } + + fun forgotPassword() { + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 0000000..89633bb --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000..5a870f5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_menu_24.xml b/app/src/main/res/drawable/baseline_menu_24.xml new file mode 100644 index 0000000..543cee9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_menu_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_notifications_24.xml b/app/src/main/res/drawable/baseline_notifications_24.xml new file mode 100644 index 0000000..1d038a4 --- /dev/null +++ b/app/src/main/res/drawable/baseline_notifications_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_people_24.xml b/app/src/main/res/drawable/baseline_people_24.xml new file mode 100644 index 0000000..a3ccd71 --- /dev/null +++ b/app/src/main/res/drawable/baseline_people_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_person_24.xml b/app/src/main/res/drawable/baseline_person_24.xml new file mode 100644 index 0000000..98730cd --- /dev/null +++ b/app/src/main/res/drawable/baseline_person_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..298a5a1 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml new file mode 100644 index 0000000..4ab79e2 --- /dev/null +++ b/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..e62d3eb --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + +