diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8c899db..a28d464 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,9 @@
plugins {
kotlinAndroid
androidApplication
+ jetbrainsKotlinSerialization version Version.Kotlin.language
+ kotlinAnnotationProcessor
+ id("com.google.dagger.hilt.android").version("2.51.1")
}
val packageName = "ru.myitschool.work"
@@ -34,4 +37,33 @@ android {
dependencies {
defaultLibrary()
+ implementation(Dependencies.AndroidX.activity)
+ implementation(Dependencies.AndroidX.fragment)
+ implementation(Dependencies.AndroidX.constraintLayout)
+
+ implementation(Dependencies.AndroidX.Navigation.fragment)
+ implementation(Dependencies.AndroidX.Navigation.navigationUi)
+
+ implementation(Dependencies.Retrofit.library)
+ implementation(Dependencies.Retrofit.gsonConverter)
+
+ implementation("com.squareup.picasso:picasso:2.8")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
+ implementation("androidx.datastore:datastore-preferences:1.1.1")
+ implementation("com.google.mlkit:barcode-scanning:17.3.0")
+
+ val cameraX = "1.3.4"
+ implementation("androidx.camera:camera-core:$cameraX")
+ implementation("androidx.camera:camera-camera2:$cameraX")
+ implementation("androidx.camera:camera-lifecycle:$cameraX")
+ implementation("androidx.camera:camera-view:$cameraX")
+ implementation("androidx.camera:camera-mlkit-vision:1.4.0-rc04")
+
+ val hilt = "2.51.1"
+ implementation("com.google.dagger:hilt-android:$hilt")
+ kapt("com.google.dagger:hilt-android-compiler:$hilt")
+}
+
+kapt {
+ correctErrorTypes = true
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9ee5c40..795af31 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,12 @@
+
+
+
+
+ tools:targetApi="31">
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt
new file mode 100644
index 0000000..3085135
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/App.kt
@@ -0,0 +1,7 @@
+package ru.myitschool.work
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class App : Application()
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt
new file mode 100644
index 0000000..8f8138d
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt
@@ -0,0 +1,5 @@
+package ru.myitschool.work.core
+
+object Constants {
+ const val SERVER_ADDRESS = "http://localhost:8090"
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/di/NetworkModule.kt b/app/src/main/java/ru/myitschool/work/data/di/NetworkModule.kt
new file mode 100644
index 0000000..acb0e74
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/di/NetworkModule.kt
@@ -0,0 +1,21 @@
+package ru.myitschool.work.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import ru.myitschool.work.core.Constants
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+ @Provides
+ fun provideRetrofit(): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(Constants.SERVER_ADDRESS)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/di/RepoModule.kt b/app/src/main/java/ru/myitschool/work/data/di/RepoModule.kt
new file mode 100644
index 0000000..4b32a3b
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/di/RepoModule.kt
@@ -0,0 +1,25 @@
+package ru.myitschool.work.data.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import ru.myitschool.work.data.repo.AccountRepositoryImpl
+import ru.myitschool.work.data.repo.AuthorizationRepositoryImpl
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import ru.myitschool.work.domain.profile.repo.UserInfoRepository
+
+@Module
+@InstallIn(ViewModelComponent::class)
+abstract class RepoModule {
+
+ @Binds
+ abstract fun bindAuthRepo(
+ impl: AuthorizationRepositoryImpl
+ ): AuthorizationRepository
+
+ @Binds
+ abstract fun bindAccountRepo(
+ impl: AccountRepositoryImpl
+ ): UserInfoRepository
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/dto/OpenQrDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/OpenQrDto.kt
new file mode 100644
index 0000000..528e1ee
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/dto/OpenQrDto.kt
@@ -0,0 +1,8 @@
+package ru.myitschool.work.data.dto
+
+import com.google.gson.annotations.SerializedName
+
+class OpenQrDto(
+ @SerializedName("value")
+ val value: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/dto/UserInfoDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/UserInfoDto.kt
new file mode 100644
index 0000000..0ffc7db
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/dto/UserInfoDto.kt
@@ -0,0 +1,14 @@
+package ru.myitschool.work.data.dto
+
+import com.google.gson.annotations.SerializedName
+
+class UserInfoDto(
+ @SerializedName("name")
+ val fullname: String?,
+ @SerializedName("photo")
+ val imageUrl: String?,
+ @SerializedName("position")
+ val position: String?,
+ @SerializedName("lastVisit")
+ val lastEntry: String?,
+)
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/mapper/UserInfoMapper.kt b/app/src/main/java/ru/myitschool/work/data/mapper/UserInfoMapper.kt
new file mode 100644
index 0000000..548b2dc
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/mapper/UserInfoMapper.kt
@@ -0,0 +1,30 @@
+package ru.myitschool.work.data.mapper
+
+import ru.myitschool.work.data.dto.UserInfoDto
+import ru.myitschool.work.domain.profile.entities.UserInfoEntity
+import java.text.SimpleDateFormat
+import java.util.Locale
+import javax.inject.Inject
+
+class UserInfoMapper @Inject constructor() {
+
+ operator fun invoke(model: UserInfoDto): Result {
+ return kotlin.runCatching {
+ UserInfoEntity(
+ fullname = model.fullname ?: error("fullname is null"),
+ imageUrl = model.imageUrl ?: error("imageUrl is null"),
+ position = model.position ?: error("position is null"),
+ lastEntryMillis = model.lastEntry?.let { date ->
+ simpleDateFormat.parse(date)?.time ?: error("parse lastEntry error")
+ } ?: error("lastEntry is null")
+ )
+ }
+ }
+
+ private companion object {
+ private val simpleDateFormat = SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss",
+ Locale.US
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AccountRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/repo/AccountRepositoryImpl.kt
new file mode 100644
index 0000000..51b9997
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/repo/AccountRepositoryImpl.kt
@@ -0,0 +1,32 @@
+package ru.myitschool.work.data.repo
+
+import dagger.Lazy
+import dagger.Reusable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import ru.myitschool.work.data.mapper.UserInfoMapper
+import ru.myitschool.work.data.source.AccountNetworkDataSource
+import ru.myitschool.work.domain.profile.entities.UserInfoEntity
+import ru.myitschool.work.domain.profile.repo.UserInfoRepository
+import javax.inject.Inject
+
+@Reusable
+class AccountRepositoryImpl @Inject constructor(
+ private val accountNetworkDataSource: AccountNetworkDataSource,
+ private val userInfoMapper: Lazy,
+): UserInfoRepository {
+ override suspend fun getInfo(username: String): Result {
+ return withContext(Dispatchers.IO) {
+ accountNetworkDataSource.getInfo(username).fold(
+ onSuccess = { value -> userInfoMapper.get().invoke(value) },
+ onFailure = { error -> Result.failure(error) }
+ )
+ }
+ }
+
+ override suspend fun openByQr(username: String, content: String): Result {
+ return withContext(Dispatchers.IO) {
+ accountNetworkDataSource.openByQr(username, content)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthorizationRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthorizationRepositoryImpl.kt
new file mode 100644
index 0000000..2229564
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthorizationRepositoryImpl.kt
@@ -0,0 +1,40 @@
+package ru.myitschool.work.data.repo
+
+import dagger.Reusable
+import ru.myitschool.work.data.source.AuthorizationNetworkDataSource
+import ru.myitschool.work.data.source.AuthorizationStorageDataSource
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import javax.inject.Inject
+import dagger.Lazy
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.withContext
+
+@Reusable
+class AuthorizationRepositoryImpl @Inject constructor(
+ private val authorizationStorageDataSource: Lazy,
+ private val authorizationNetworkDataSource: Lazy,
+): AuthorizationRepository {
+
+ override suspend fun login(username: String): Result {
+ return withContext(Dispatchers.IO) {
+ authorizationNetworkDataSource.get().checkLogin(username)
+ .onSuccess {
+ authorizationStorageDataSource.get().updateLogin(username)
+ }
+ }
+ }
+
+ override suspend fun logout() {
+ authorizationStorageDataSource.get().updateLogin(null)
+ }
+
+ override suspend fun getLogin(): Result {
+ val result = authorizationStorageDataSource.get().login.firstOrNull()
+ return if (result == null) {
+ Result.failure(Exception("Not authorize"))
+ } else {
+ Result.success(result)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/source/AccountApi.kt b/app/src/main/java/ru/myitschool/work/data/source/AccountApi.kt
new file mode 100644
index 0000000..2a5101c
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/source/AccountApi.kt
@@ -0,0 +1,21 @@
+package ru.myitschool.work.data.source
+
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.PATCH
+import retrofit2.http.Path
+import ru.myitschool.work.data.dto.OpenQrDto
+import ru.myitschool.work.data.dto.UserInfoDto
+
+interface AccountApi {
+ @GET("api/{username}/info")
+ suspend fun getInfo(
+ @Path("username") username: String
+ ) : UserInfoDto
+
+ @PATCH("api/{username}/open")
+ suspend fun openByQr(
+ @Path("username") username: String,
+ @Body content: OpenQrDto
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/source/AccountNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/AccountNetworkDataSource.kt
new file mode 100644
index 0000000..4010778
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/source/AccountNetworkDataSource.kt
@@ -0,0 +1,29 @@
+package ru.myitschool.work.data.source
+
+import dagger.Reusable
+import retrofit2.Retrofit
+import ru.myitschool.work.data.dto.OpenQrDto
+import ru.myitschool.work.data.dto.UserInfoDto
+import javax.inject.Inject
+
+@Reusable
+class AccountNetworkDataSource @Inject constructor(
+ private val retrofit: Retrofit
+) {
+ private val api by lazy {
+ retrofit.create(AccountApi::class.java)
+ }
+
+ suspend fun getInfo(username: String): Result {
+ return kotlin.runCatching { api.getInfo(username = username) }
+ }
+
+ suspend fun openByQr(username: String, content: String): Result {
+ return kotlin.runCatching {
+ api.openByQr(
+ username = username,
+ content = OpenQrDto(value = content)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/source/AuthorizationApi.kt b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationApi.kt
new file mode 100644
index 0000000..38ce0e9
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationApi.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.data.source
+
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface AuthorizationApi {
+ @GET("api/{username}/auth")
+ suspend fun checkLogin(
+ @Path("username") username: String
+ ) : Response
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/source/AuthorizationNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationNetworkDataSource.kt
new file mode 100644
index 0000000..a728203
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationNetworkDataSource.kt
@@ -0,0 +1,27 @@
+package ru.myitschool.work.data.source
+
+import android.accounts.NetworkErrorException
+import dagger.Reusable
+import retrofit2.Retrofit
+import javax.inject.Inject
+
+@Reusable
+class AuthorizationNetworkDataSource @Inject constructor(
+ private val retrofit: Retrofit
+) {
+ private val api by lazy {
+ retrofit.create(AuthorizationApi::class.java)
+ }
+
+ suspend fun checkLogin(username: String): Result {
+ return kotlin.runCatching { api.checkLogin(username = username) }.fold(
+ onSuccess = { response ->
+ when (response.code()) {
+ 200 -> Result.success(Unit)
+ else -> Result.failure(NetworkErrorException("Error ${response.code()}"))
+ }
+ },
+ onFailure = { error -> Result.failure(error) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/data/source/AuthorizationStorageDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationStorageDataSource.kt
new file mode 100644
index 0000000..d4ce936
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/data/source/AuthorizationStorageDataSource.kt
@@ -0,0 +1,38 @@
+package ru.myitschool.work.data.source
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AuthorizationStorageDataSource @Inject constructor(
+ @ApplicationContext private val context: Context,
+) {
+ private val Context.storage: DataStore by preferencesDataStore(name = NAME)
+ val login: Flow = context.storage.data.map { preferences ->
+ preferences[LOGIN_KEY]
+ }
+
+ suspend fun updateLogin(username: String?) {
+ context.storage.edit { settings ->
+ if (username != null) {
+ settings[LOGIN_KEY] = username
+ } else {
+ settings.remove(LOGIN_KEY)
+ }
+ }
+ }
+
+ private companion object {
+ const val NAME = "auth_data"
+ val LOGIN_KEY = stringPreferencesKey("login")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckValidLoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckValidLoginUseCase.kt
new file mode 100644
index 0000000..f5352da
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckValidLoginUseCase.kt
@@ -0,0 +1,16 @@
+package ru.myitschool.work.domain.auth
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class CheckValidLoginUseCase @Inject constructor() {
+ suspend operator fun invoke(login: String): Boolean {
+ return withContext(Dispatchers.Default) {
+ login.isNotBlank() &&
+ login.length >= 3 &&
+ !login[0].isDigit() &&
+ login.all { char -> char.isLetterOrDigit() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetLoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetLoginUseCase.kt
new file mode 100644
index 0000000..6502495
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetLoginUseCase.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.domain.auth
+
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import javax.inject.Inject
+
+class GetLoginUseCase @Inject constructor(
+ private val repo: AuthorizationRepository,
+) {
+ suspend operator fun invoke(): Result {
+ return repo.getLogin()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/IsLoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/IsLoginUseCase.kt
new file mode 100644
index 0000000..15067ed
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/IsLoginUseCase.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.domain.auth
+
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import javax.inject.Inject
+
+class IsLoginUseCase @Inject constructor(
+ private val repo: AuthorizationRepository,
+) {
+ suspend operator fun invoke(): Boolean {
+ return repo.getLogin().isSuccess
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/LoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/LoginUseCase.kt
new file mode 100644
index 0000000..62b593d
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/LoginUseCase.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.domain.auth
+
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import javax.inject.Inject
+
+class LoginUseCase @Inject constructor(
+ private val repo: AuthorizationRepository,
+) {
+ suspend operator fun invoke(login: String): Result {
+ return repo.login(username = login)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/LogoutUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/LogoutUseCase.kt
new file mode 100644
index 0000000..0e583c2
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/LogoutUseCase.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.domain.auth
+
+import ru.myitschool.work.domain.auth.repo.AuthorizationRepository
+import javax.inject.Inject
+
+class LogoutUseCase @Inject constructor(
+ private val repo: AuthorizationRepository,
+) {
+ suspend operator fun invoke() {
+ return repo.logout()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/repo/AuthorizationRepository.kt b/app/src/main/java/ru/myitschool/work/domain/auth/repo/AuthorizationRepository.kt
new file mode 100644
index 0000000..7b4bab8
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/auth/repo/AuthorizationRepository.kt
@@ -0,0 +1,8 @@
+package ru.myitschool.work.domain.auth.repo
+
+interface AuthorizationRepository {
+ suspend fun login(username: String): Result
+ suspend fun logout()
+
+ suspend fun getLogin(): Result
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/profile/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/profile/GetUserInfoUseCase.kt
new file mode 100644
index 0000000..d9cda8f
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/profile/GetUserInfoUseCase.kt
@@ -0,0 +1,18 @@
+package ru.myitschool.work.domain.profile
+
+import ru.myitschool.work.domain.auth.GetLoginUseCase
+import ru.myitschool.work.domain.profile.entities.UserInfoEntity
+import ru.myitschool.work.domain.profile.repo.UserInfoRepository
+import javax.inject.Inject
+
+class GetUserInfoUseCase @Inject constructor(
+ private val repo: UserInfoRepository,
+ private val getLoginUseCase: GetLoginUseCase,
+) {
+ suspend operator fun invoke(): Result {
+ return getLoginUseCase().fold(
+ onSuccess = { username -> repo.getInfo(username = username) },
+ onFailure = { error -> Result.failure(error) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/profile/OpenByQrUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/profile/OpenByQrUseCase.kt
new file mode 100644
index 0000000..8e6d8ae
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/profile/OpenByQrUseCase.kt
@@ -0,0 +1,17 @@
+package ru.myitschool.work.domain.profile
+
+import ru.myitschool.work.domain.auth.GetLoginUseCase
+import ru.myitschool.work.domain.profile.repo.UserInfoRepository
+import javax.inject.Inject
+
+class OpenByQrUseCase @Inject constructor(
+ private val repo: UserInfoRepository,
+ private val getLoginUseCase: GetLoginUseCase,
+) {
+ suspend operator fun invoke(content: String): Result {
+ return getLoginUseCase().fold(
+ onSuccess = { username -> repo.openByQr(username = username, content = content) },
+ onFailure = { error -> Result.failure(error) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/profile/entities/UserInfoEntity.kt b/app/src/main/java/ru/myitschool/work/domain/profile/entities/UserInfoEntity.kt
new file mode 100644
index 0000000..46dc55f
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/profile/entities/UserInfoEntity.kt
@@ -0,0 +1,8 @@
+package ru.myitschool.work.domain.profile.entities
+
+class UserInfoEntity(
+ val fullname: String,
+ val imageUrl: String,
+ val position: String,
+ val lastEntryMillis: Long,
+)
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/domain/profile/repo/UserInfoRepository.kt b/app/src/main/java/ru/myitschool/work/domain/profile/repo/UserInfoRepository.kt
new file mode 100644
index 0000000..d10df45
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/domain/profile/repo/UserInfoRepository.kt
@@ -0,0 +1,9 @@
+package ru.myitschool.work.domain.profile.repo
+
+import ru.myitschool.work.domain.profile.entities.UserInfoEntity
+
+interface UserInfoRepository {
+ suspend fun getInfo(username: String) : Result
+
+ suspend fun openByQr(username: String, content: String) : Result
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt
new file mode 100644
index 0000000..ba0cd96
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt
@@ -0,0 +1,64 @@
+package ru.myitschool.work.ui
+
+import android.os.Bundle
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.createGraph
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.fragment.fragment
+import dagger.hilt.android.AndroidEntryPoint
+import ru.myitschool.work.R
+import ru.myitschool.work.ui.login.LoginDestination
+import ru.myitschool.work.ui.login.LoginFragment
+import ru.myitschool.work.ui.profile.ProfileDestination
+import ru.myitschool.work.ui.profile.ProfileFragment
+import ru.myitschool.work.ui.qr.result.QrResultDestination
+import ru.myitschool.work.ui.qr.result.QrResultFragment
+import ru.myitschool.work.ui.qr.scan.QrScanDestination
+import ru.myitschool.work.ui.qr.scan.QrScanFragment
+import ru.myitschool.work.ui.splash.SplashDestination
+import ru.myitschool.work.ui.splash.SplashFragment
+
+@AndroidEntryPoint
+class RootActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_root)
+
+ val navHostFragment = supportFragmentManager
+ .findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
+
+ if (navHostFragment != null) {
+ val navController = navHostFragment.navController
+ navController.graph = navController.createGraph(
+ startDestination = SplashDestination
+ ) {
+ fragment()
+ fragment()
+ fragment()
+ fragment()
+ fragment()
+ }
+ }
+
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ onSupportNavigateUp()
+ }
+ }
+ )
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ val navController = findNavController(R.id.nav_host_fragment)
+ val popBackResult = if (navController.previousBackStackEntry != null) {
+ navController.popBackStack()
+ } else {
+ false
+ }
+ return popBackResult || super.onSupportNavigateUp()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt
new file mode 100644
index 0000000..50acfb0
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt
@@ -0,0 +1,6 @@
+package ru.myitschool.work.ui.login
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object LoginDestination
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt
new file mode 100644
index 0000000..aea92d6
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt
@@ -0,0 +1,73 @@
+package ru.myitschool.work.ui.login
+
+import android.os.Bundle
+import android.text.Editable
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import ru.myitschool.work.R
+import ru.myitschool.work.databinding.FragmentLoginBinding
+import ru.myitschool.work.ui.profile.ProfileDestination
+import ru.myitschool.work.utils.TextChangedListener
+import ru.myitschool.work.utils.collectWhenStarted
+import ru.myitschool.work.utils.visibleOrGone
+
+@AndroidEntryPoint
+class LoginFragment : Fragment(R.layout.fragment_login) {
+ private var _binding: FragmentLoginBinding? = null
+ private val binding: FragmentLoginBinding get() = _binding!!
+
+ private val viewModel: LoginViewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentLoginBinding.bind(view)
+ initCallback()
+ subscribe()
+ }
+
+ private fun subscribe() {
+ viewModel.state.collectWhenStarted(this) { state ->
+ binding.error.visibleOrGone(state is LoginViewModel.State.Error)
+ binding.loading.visibleOrGone(state is LoginViewModel.State.Loading)
+ binding.login.visibleOrGone(state !is LoginViewModel.State.Loading)
+ binding.username.isEnabled = state !is LoginViewModel.State.Loading
+ when (state) {
+ is LoginViewModel.State.Loading -> Unit
+ is LoginViewModel.State.Error -> {
+ binding.error.text = state.errorText
+ }
+
+ is LoginViewModel.State.Show -> {
+ binding.login.isEnabled = state.isLoginButtonEnabled
+ }
+ }
+ }
+
+ viewModel.action.collectWhenStarted(this) { action ->
+ when (action) {
+ is LoginViewModel.Action.OpenProfile -> {
+ findNavController().navigate(ProfileDestination) {
+ popUpTo { inclusive = true }
+ }
+ }
+ }
+ }
+ }
+
+ private fun initCallback() {
+ binding.login.setOnClickListener { viewModel.clickLogin() }
+ binding.username.addTextChangedListener(object : TextChangedListener() {
+ override fun afterTextChanged(s: Editable?) {
+ viewModel.inputLogin(s.toString())
+ }
+ })
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ super.onDestroyView()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt
new file mode 100644
index 0000000..1f62bc0
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt
@@ -0,0 +1,86 @@
+package ru.myitschool.work.ui.login
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.Lazy
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import ru.myitschool.work.R
+import ru.myitschool.work.domain.auth.CheckValidLoginUseCase
+import ru.myitschool.work.domain.auth.LoginUseCase
+import ru.myitschool.work.utils.MutablePublishFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val checkValidLoginUseCase: Lazy,
+ private val loginUseCase: Lazy,
+) : ViewModel() {
+
+ private val _action = MutablePublishFlow()
+ val action = _action.asSharedFlow()
+
+ private val _state = MutableStateFlow(initialState)
+ val state = _state.asStateFlow()
+
+ private var login: String = ""
+
+ fun clickLogin() {
+ viewModelScope.launch {
+ _state.update { State.Loading }
+ loginUseCase.get().invoke(login = login).fold(
+ onSuccess = {
+ _action.emit(Action.OpenProfile)
+ },
+ onFailure = { error ->
+ _state.update {
+ State.Error(
+ errorText = error.localizedMessage
+ ?: context.resources.getString(R.string.login_error)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ fun inputLogin(login: String) {
+ this.login = login
+ viewModelScope.launch {
+ _state.update {
+ State.Show(
+ isLoginButtonEnabled = checkValidLoginUseCase.get().invoke(login = login)
+ )
+ }
+ }
+ }
+
+ sealed interface Action {
+ data object OpenProfile : Action
+ }
+
+ sealed interface State {
+ data object Loading : State
+
+ data class Error(
+ val errorText: String,
+ ) : State
+
+ data class Show(
+ val isLoginButtonEnabled: Boolean
+ ) : State
+ }
+
+ private companion object {
+ val initialState = State.Show(
+ isLoginButtonEnabled = false
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/ProfileDestination.kt b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileDestination.kt
new file mode 100644
index 0000000..83c7a80
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileDestination.kt
@@ -0,0 +1,6 @@
+package ru.myitschool.work.ui.profile
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object ProfileDestination
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/ProfileFragment.kt b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileFragment.kt
new file mode 100644
index 0000000..d1d6ba1
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileFragment.kt
@@ -0,0 +1,83 @@
+package ru.myitschool.work.ui.profile
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.squareup.picasso.Picasso
+import dagger.hilt.android.AndroidEntryPoint
+import ru.myitschool.work.R
+import ru.myitschool.work.databinding.FragmentProfileBinding
+import ru.myitschool.work.ui.login.LoginDestination
+import ru.myitschool.work.ui.qr.result.QrResultDestination
+import ru.myitschool.work.ui.qr.scan.QrScanDestination
+import ru.myitschool.work.utils.collectWhenStarted
+import ru.myitschool.work.utils.visibleOrGone
+
+@AndroidEntryPoint
+class ProfileFragment : Fragment(R.layout.fragment_profile) {
+ private var _binding: FragmentProfileBinding? = null
+ private val binding: FragmentProfileBinding get() = _binding!!
+
+ private val viewModel: ProfileViewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentProfileBinding.bind(view)
+ initCallback()
+ subscribe()
+ }
+
+ private fun subscribe() {
+ viewModel.state.collectWhenStarted(this) { state ->
+ binding.showState.visibleOrGone(state is ProfileViewModel.State.Show)
+ binding.error.visibleOrGone(state is ProfileViewModel.State.Error)
+ binding.loading.visibleOrGone(state is ProfileViewModel.State.Loading)
+
+ when(state) {
+ is ProfileViewModel.State.Loading -> Unit
+ is ProfileViewModel.State.Error -> {
+ binding.error.text = state.errorText
+ }
+ is ProfileViewModel.State.Show -> {
+ binding.fullname.text = state.fullname
+ binding.position.text = state.position
+ binding.lastEntry.text = state.lastEntry
+ Picasso.get()
+ .load(state.imageUrl)
+ .error(R.drawable.ic_no_img)
+ .into(binding.photo)
+ }
+ }
+ }
+
+ viewModel.action.collectWhenStarted(this) { action ->
+ when(action) {
+ is ProfileViewModel.Action.OpenLogin -> {
+ findNavController().navigate(LoginDestination) {
+ popUpTo { inclusive = true }
+ }
+ }
+
+ is ProfileViewModel.Action.OpenScan -> {
+ findNavController().apply {
+ navigate(QrResultDestination)
+ navigate(QrScanDestination)
+ }
+ }
+ }
+ }
+ }
+
+ private fun initCallback() {
+ binding.refresh.setOnClickListener { viewModel.clickRefresh() }
+ binding.logout.setOnClickListener { viewModel.clickLogout() }
+ binding.scan.setOnClickListener { viewModel.clickScan() }
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ super.onDestroyView()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/ProfileViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileViewModel.kt
new file mode 100644
index 0000000..2c51899
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/profile/ProfileViewModel.kt
@@ -0,0 +1,111 @@
+package ru.myitschool.work.ui.profile
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.Lazy
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import ru.myitschool.work.R
+import ru.myitschool.work.domain.auth.LogoutUseCase
+import ru.myitschool.work.domain.profile.GetUserInfoUseCase
+import ru.myitschool.work.utils.MutablePublishFlow
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+
+@HiltViewModel
+class ProfileViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val getUserInfoUseCase: Lazy,
+ private val logoutUseCase: Lazy,
+) : ViewModel() {
+
+ private val _action = MutablePublishFlow()
+ val action = _action.asSharedFlow()
+
+ private val _state = MutableStateFlow(initialState)
+ val state = _state.asStateFlow()
+
+ init {
+ updateUserInfo()
+ }
+
+ fun clickRefresh() {
+ updateUserInfo()
+ }
+
+ fun clickLogout() {
+ viewModelScope.launch {
+ logoutUseCase.get().invoke()
+ _action.emit(Action.OpenLogin)
+ }
+ }
+
+ fun clickScan() {
+ viewModelScope.launch {
+ _action.emit(Action.OpenScan)
+ }
+ }
+
+ private fun updateUserInfo() {
+ viewModelScope.launch {
+ _state.update { State.Loading }
+ getUserInfoUseCase.get().invoke().fold(
+ onSuccess = { value ->
+ _state.update {
+ State.Show(
+ fullname = value.fullname,
+ imageUrl = value.imageUrl,
+ position = value.position,
+ lastEntry = simpleDateFormat.format(Date(value.lastEntryMillis))
+ )
+ }
+ },
+ onFailure = { error ->
+ _state.update {
+ State.Error(
+ errorText = error.localizedMessage
+ ?: context.resources.getString(R.string.login_error)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ sealed interface State {
+ data object Loading : State
+
+ data class Error(
+ val errorText: String,
+ ) : State
+
+ data class Show(
+ val fullname: String,
+ val imageUrl: String,
+ val position: String,
+ val lastEntry: String,
+ ) : State
+ }
+
+ sealed interface Action {
+ data object OpenLogin : Action
+ data object OpenScan : Action
+ }
+
+ private companion object {
+ private val simpleDateFormat = SimpleDateFormat(
+ "yyyy-MM-DD HH:mm",
+ Locale.US
+ )
+
+ val initialState = State.Loading
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultDestination.kt
new file mode 100644
index 0000000..1cde936
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultDestination.kt
@@ -0,0 +1,6 @@
+package ru.myitschool.work.ui.qr.result
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+object QrResultDestination
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt
new file mode 100644
index 0000000..30ebddc
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt
@@ -0,0 +1,64 @@
+package ru.myitschool.work.ui.qr.result
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import ru.myitschool.work.R
+import ru.myitschool.work.databinding.FragmentQrResultBinding
+import ru.myitschool.work.ui.qr.scan.QrScanDestination
+import ru.myitschool.work.utils.collectWhenStarted
+import ru.myitschool.work.utils.visibleOrGone
+
+@AndroidEntryPoint
+class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
+ private var _binding: FragmentQrResultBinding? = null
+ private val binding: FragmentQrResultBinding get() = _binding!!
+
+ private val viewModel: QrResultViewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentQrResultBinding.bind(view)
+ initCallback()
+ subscribe()
+ }
+
+ private fun subscribe() {
+ viewModel.state.collectWhenStarted(this) { state ->
+ binding.loading.visibleOrGone(state is QrResultViewModel.State.Loading)
+ binding.status.visibleOrGone(state is QrResultViewModel.State.Show)
+ when (state) {
+ is QrResultViewModel.State.Loading -> Unit
+ is QrResultViewModel.State.Show -> {
+ binding.status.text = state.status
+ }
+ }
+ }
+
+ viewModel.action.collectWhenStarted(this) { action ->
+ when (action) {
+ is QrResultViewModel.Action.Close -> {
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+
+ private fun initCallback() {
+ binding.close.setOnClickListener { viewModel.clickClose() }
+ findNavController().currentBackStackEntry
+ ?.savedStateHandle
+ ?.getLiveData(QrScanDestination.REQUEST_KEY)
+ ?.observe(viewLifecycleOwner) { result ->
+ viewModel.setScanResult(QrScanDestination.getDataIfExist(result))
+ }
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ super.onDestroyView()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt
new file mode 100644
index 0000000..27e5ac1
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt
@@ -0,0 +1,71 @@
+package ru.myitschool.work.ui.qr.result
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import ru.myitschool.work.domain.profile.OpenByQrUseCase
+import ru.myitschool.work.utils.MutablePublishFlow
+import javax.inject.Inject
+import dagger.Lazy
+import ru.myitschool.work.R
+
+@HiltViewModel
+class QrResultViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val openByQrUseCase: Lazy,
+) : ViewModel() {
+
+ private val _action = MutablePublishFlow()
+ val action = _action.asSharedFlow()
+
+ private val _state = MutableStateFlow(initialState)
+ val state = _state.asStateFlow()
+
+ fun clickClose() {
+ viewModelScope.launch {
+ _action.emit(Action.Close)
+ }
+ }
+
+ fun setScanResult(content: String?) {
+ viewModelScope.launch {
+ _state.update { State.Loading }
+ val statusText = if (content == null) {
+ R.string.qr_result_status_cancel
+ } else {
+ openByQrUseCase.get().invoke(content).fold(
+ onSuccess = { R.string.qr_result_status_success },
+ onFailure = { R.string.qr_result_status_error }
+ )
+ }
+ _state.update {
+ State.Show(
+ status = context.resources.getString(statusText)
+ )
+ }
+ }
+ }
+
+ sealed interface Action {
+ data object Close : Action
+ }
+
+ sealed interface State {
+ data object Loading : State
+
+ data class Show(
+ val status: String
+ ) : State
+ }
+
+ private companion object {
+ val initialState = State.Loading
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt
new file mode 100644
index 0000000..d7b8e6d
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt
@@ -0,0 +1,29 @@
+package ru.myitschool.work.ui.qr.scan
+
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object QrScanDestination {
+ const val REQUEST_KEY = "qr_result"
+ private const val KEY_QR_DATA = "key_qr"
+
+ fun newInstance(): QrScanFragment {
+ return QrScanFragment()
+ }
+
+ fun getDataIfExist(bundle: Bundle): String? {
+ return if (bundle.containsKey(KEY_QR_DATA)) {
+ bundle.getString(KEY_QR_DATA)
+ } else {
+ null
+ }
+ }
+
+ internal fun packToBundle(data: String): Bundle {
+ return bundleOf(
+ KEY_QR_DATA to data
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt
new file mode 100644
index 0000000..38beb4d
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt
@@ -0,0 +1,138 @@
+package ru.myitschool.work.ui.qr.scan
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.mlkit.vision.MlKitAnalyzer
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.setFragmentResult
+import androidx.fragment.app.viewModels
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
+import com.google.mlkit.vision.barcode.BarcodeScanner
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+import ru.myitschool.work.R
+import ru.myitschool.work.databinding.FragmentQrScanBinding
+import ru.myitschool.work.utils.collectWhenStarted
+import ru.myitschool.work.utils.visibleOrGone
+
+class QrScanFragment : Fragment(R.layout.fragment_qr_scan) {
+ private var _binding: FragmentQrScanBinding? = null
+ private val binding: FragmentQrScanBinding get() = _binding!!
+
+ private var barcodeScanner: BarcodeScanner? = null
+ private var isCameraInit: Boolean = false
+ private val permissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted -> viewModel.onPermissionResult(isGranted) }
+
+ private val viewModel: QrScanViewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentQrScanBinding.bind(view)
+ sendResult(bundleOf())
+ subscribe()
+ initCallback()
+ }
+
+ private fun initCallback() {
+ binding.close.setOnClickListener { viewModel.close() }
+ }
+
+ private fun subscribe() {
+ viewModel.state.collectWhenStarted(this) { state ->
+ binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading)
+ binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan)
+ if (!isCameraInit && state is QrScanViewModel.State.Scan) {
+ startCamera()
+ isCameraInit = true
+ }
+ }
+
+ viewModel.action.collectWhenStarted(this) { action ->
+ when (action) {
+ is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission)
+ is QrScanViewModel.Action.CloseWithCancel -> {
+ goBack()
+ }
+ is QrScanViewModel.Action.CloseWithResult -> {
+ sendResult(QrScanDestination.packToBundle(action.result))
+ goBack()
+ }
+ }
+ }
+ }
+
+ private fun requestPermission(permission: String) {
+ permissionLauncher.launch(permission)
+ }
+
+ private fun startCamera() {
+ val context = requireContext()
+ val cameraController = LifecycleCameraController(context)
+ val previewView: PreviewView = binding.viewFinder
+ val executor = ContextCompat.getMainExecutor(context)
+
+ val options = BarcodeScannerOptions.Builder()
+ .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
+ .build()
+ val barcodeScanner = BarcodeScanning.getClient(options)
+ this.barcodeScanner = barcodeScanner
+
+ cameraController.setImageAnalysisAnalyzer(
+ executor,
+ MlKitAnalyzer(
+ listOf(barcodeScanner),
+ ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED,
+ executor
+ ) { result ->
+ result?.getValue(barcodeScanner)?.firstOrNull()?.let { value ->
+ viewModel.findBarcode(value)
+
+ }
+ }
+ )
+
+ cameraController.bindToLifecycle(this)
+ previewView.controller = cameraController
+ }
+
+ override fun onDestroyView() {
+ barcodeScanner?.close()
+ barcodeScanner = null
+ _binding = null
+ super.onDestroyView()
+ }
+
+ private fun goBack() {
+ findNavControllerOrNull()?.popBackStack()
+ ?: requireActivity().onBackPressedDispatcher.onBackPressed()
+ }
+
+ private fun sendResult(bundle: Bundle) {
+ setFragmentResult(
+ QrScanDestination.REQUEST_KEY,
+ bundle
+ )
+ findNavControllerOrNull()
+ ?.previousBackStackEntry
+ ?.savedStateHandle
+ ?.set(QrScanDestination.REQUEST_KEY, bundle)
+ }
+
+ private fun findNavControllerOrNull(): NavController? {
+ return try {
+ findNavController()
+ } catch (_: Throwable) {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt
new file mode 100644
index 0000000..e4fd4da
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt
@@ -0,0 +1,92 @@
+package ru.myitschool.work.ui.qr.scan
+
+import android.Manifest
+import android.app.Application
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.mlkit.vision.barcode.common.Barcode
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import ru.myitschool.work.utils.MutablePublishFlow
+
+class QrScanViewModel(
+ application: Application
+) : AndroidViewModel(application) {
+
+ private val _action = MutablePublishFlow()
+ val action = _action.asSharedFlow()
+
+ private val _state = MutableStateFlow(initialState)
+ val state = _state.asStateFlow()
+
+ init {
+ checkPermission()
+ }
+
+ fun onPermissionResult(isGranted: Boolean) {
+ viewModelScope.launch {
+ if (isGranted) {
+ _state.update { State.Scan }
+ } else {
+ _action.emit(Action.CloseWithCancel)
+ }
+ }
+ }
+
+ private fun checkPermission() {
+ viewModelScope.launch {
+ val isPermissionGranted = ContextCompat.checkSelfPermission(
+ getApplication(),
+ CAMERA_PERMISSION
+ ) == PackageManager.PERMISSION_GRANTED
+ if (isPermissionGranted) {
+ _state.update { State.Scan }
+ } else {
+ delay(1000)
+ _action.emit(Action.RequestPermission(CAMERA_PERMISSION))
+ }
+ }
+ }
+
+ fun findBarcode(barcode: Barcode) {
+ viewModelScope.launch {
+ barcode.rawValue?.let { value ->
+ _action.emit(Action.CloseWithResult(value))
+ }
+ }
+ }
+
+ fun close() {
+ viewModelScope.launch {
+ _action.emit(Action.CloseWithCancel)
+ }
+ }
+
+ sealed interface State {
+ data object Loading : State
+
+ data object Scan : State
+ }
+
+ sealed interface Action {
+ data class RequestPermission(
+ val permission: String
+ ) : Action
+ data object CloseWithCancel : Action
+ data class CloseWithResult(
+ val result: String
+ ) : Action
+ }
+
+ private companion object {
+ val initialState = State.Loading
+
+ const val CAMERA_PERMISSION = Manifest.permission.CAMERA
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/splash/SplashDestination.kt b/app/src/main/java/ru/myitschool/work/ui/splash/SplashDestination.kt
new file mode 100644
index 0000000..b0a6e34
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/splash/SplashDestination.kt
@@ -0,0 +1,6 @@
+package ru.myitschool.work.ui.splash
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object SplashDestination
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/splash/SplashFragment.kt b/app/src/main/java/ru/myitschool/work/ui/splash/SplashFragment.kt
new file mode 100644
index 0000000..bcd9f3e
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/splash/SplashFragment.kt
@@ -0,0 +1,50 @@
+package ru.myitschool.work.ui.splash
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import ru.myitschool.work.R
+import ru.myitschool.work.databinding.FragmentSplashBinding
+import ru.myitschool.work.ui.login.LoginDestination
+import ru.myitschool.work.ui.profile.ProfileDestination
+import ru.myitschool.work.utils.collectWhenStarted
+
+@AndroidEntryPoint
+class SplashFragment : Fragment(R.layout.fragment_splash) {
+ private var _binding: FragmentSplashBinding? = null
+ private val binding: FragmentSplashBinding get() = _binding!!
+
+ private val viewModel: SplashViewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentSplashBinding.bind(view)
+ subscribe()
+ }
+
+ private fun subscribe() {
+ viewModel.action.collectWhenStarted(this) { action ->
+ when (action) {
+ SplashViewModel.Action.OpenProfile -> {
+ findNavController().navigate(ProfileDestination) {
+ popUpTo(SplashDestination) { inclusive = true }
+ }
+ }
+
+ SplashViewModel.Action.OpenLogin -> {
+ findNavController().navigate(LoginDestination) {
+ popUpTo { inclusive = true }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ _binding = null
+ super.onDestroyView()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/ui/splash/SplashViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/splash/SplashViewModel.kt
new file mode 100644
index 0000000..1eb0fbc
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/ui/splash/SplashViewModel.kt
@@ -0,0 +1,35 @@
+package ru.myitschool.work.ui.splash
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.Lazy
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import ru.myitschool.work.domain.auth.IsLoginUseCase
+import ru.myitschool.work.utils.MutablePublishFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class SplashViewModel @Inject constructor(
+ private val isLoginUseCase: Lazy,
+) : ViewModel() {
+
+ private val _action = MutablePublishFlow()
+ val action = _action.asSharedFlow()
+
+ init {
+ viewModelScope.launch {
+ if (isLoginUseCase.get().invoke()) {
+ _action.emit(Action.OpenProfile)
+ } else {
+ _action.emit(Action.OpenLogin)
+ }
+ }
+ }
+
+ sealed interface Action {
+ data object OpenProfile : Action
+ data object OpenLogin : Action
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt
new file mode 100644
index 0000000..87bccc2
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt
@@ -0,0 +1,10 @@
+package ru.myitschool.work.utils
+
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+fun MutablePublishFlow() = MutableSharedFlow(
+ replay = 0,
+ extraBufferCapacity = 1,
+ BufferOverflow.DROP_OLDEST
+)
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt
new file mode 100644
index 0000000..8c99ef3
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt
@@ -0,0 +1,18 @@
+package ru.myitschool.work.utils
+
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+inline fun Flow.collectWhenStarted(
+ fragment: Fragment,
+ crossinline collector: (T) -> Unit
+) {
+ fragment.viewLifecycleOwner.lifecycleScope.launch {
+ flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value ->
+ collector.invoke(value)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt
new file mode 100644
index 0000000..c81147d
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt
@@ -0,0 +1,12 @@
+package ru.myitschool.work.utils
+
+import android.text.Editable
+import android.text.TextWatcher
+
+open class TextChangedListener: TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+
+ override fun afterTextChanged(s: Editable?) = Unit
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt
new file mode 100644
index 0000000..5c38f67
--- /dev/null
+++ b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt
@@ -0,0 +1,7 @@
+package ru.myitschool.work.utils
+
+import android.view.View
+
+fun View.visibleOrGone(isVisible: Boolean) {
+ this.visibility = if (isVisible) View.VISIBLE else View.GONE
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 0000000..f8ca0c6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml
new file mode 100644
index 0000000..c22a96f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logout.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_no_img.xml b/app/src/main/res/drawable/ic_no_img.xml
new file mode 100644
index 0000000..44206c9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_no_img.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml
new file mode 100644
index 0000000..b03f9ae
--- /dev/null
+++ b/app/src/main/res/drawable/ic_qr_code.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000..86504d0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml
new file mode 100644
index 0000000..e7cb1a9
--- /dev/null
+++ b/app/src/main/res/layout/activity_root.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml
new file mode 100644
index 0000000..4ce5199
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml
new file mode 100644
index 0000000..02a3619
--- /dev/null
+++ b/app/src/main/res/layout/fragment_profile.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_qr_result.xml b/app/src/main/res/layout/fragment_qr_result.xml
new file mode 100644
index 0000000..8f4c92b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_qr_result.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_qr_scan.xml b/app/src/main/res/layout/fragment_qr_scan.xml
new file mode 100644
index 0000000..a52eb71
--- /dev/null
+++ b/app/src/main/res/layout/fragment_qr_scan.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml
new file mode 100644
index 0000000..6091984
--- /dev/null
+++ b/app/src/main/res/layout/fragment_splash.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..ce65075
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 96034ac..b183019 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- Work
+ NTO Pass
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_login.xml b/app/src/main/res/values/strings_login.xml
new file mode 100644
index 0000000..e046352
--- /dev/null
+++ b/app/src/main/res/values/strings_login.xml
@@ -0,0 +1,6 @@
+
+
+ Username
+ Login
+ Something wrong
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_profile.xml b/app/src/main/res/values/strings_profile.xml
new file mode 100644
index 0000000..1c399af
--- /dev/null
+++ b/app/src/main/res/values/strings_profile.xml
@@ -0,0 +1,5 @@
+
+
+ Refresh
+ Scan QR
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_qr.xml b/app/src/main/res/values/strings_qr.xml
new file mode 100644
index 0000000..ce50067
--- /dev/null
+++ b/app/src/main/res/values/strings_qr.xml
@@ -0,0 +1,4 @@
+
+
+ Close
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_qr_result.xml b/app/src/main/res/values/strings_qr_result.xml
new file mode 100644
index 0000000..57f5c2c
--- /dev/null
+++ b/app/src/main/res/values/strings_qr_result.xml
@@ -0,0 +1,6 @@
+
+
+ Success
+ Operation was cancelled
+ Something wrong
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 64d8748..4a92e0e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,6 @@
plugins {
androidApplication version Version.agp apply false
kotlinJvm version Version.Kotlin.language apply false
+ kotlinAnnotationProcessor version Version.Kotlin.language apply false
+ id("com.google.dagger.hilt.android") version "2.51.1" apply false
}
\ No newline at end of file
diff --git a/buildSrc b/buildSrc
index d959060..ec48d5f 160000
--- a/buildSrc
+++ b/buildSrc
@@ -1 +1 @@
-Subproject commit d9590600045906edeb852eaa3f0b9bf7d1875813
+Subproject commit ec48d5f6b8c45e8058303282e9ec1c1d0ed02989