commit a7ba596d97152bf643911e318b25a970d5264b63 Author: Andrey Date: Tue Feb 18 19:40:34 2025 +0300 Initial commit diff --git a/.gitea/workflows/automerge-with-core.yaml b/.gitea/workflows/automerge-with-core.yaml new file mode 100644 index 0000000..e040dbd --- /dev/null +++ b/.gitea/workflows/automerge-with-core.yaml @@ -0,0 +1,26 @@ +name: Merge core/template-android-project to this repo + +env: + CORE_REPO: "https://git.sicampus.ru/core/template-android-project.git" + TOKEN: ${{ secrets.PUSH_TOKEN }} + +run-name: Merge core/template-android-project to ${{ gitea.repository }} +on: + schedule: + - cron: '@daily' + + +jobs: + merge-if-needed: + if: ${{ !contains(gitea.repository, 'core/template-android-project' ) }} + runs-on: ubuntu-latest + steps: + - run: echo "Merge core/template-android-project to ${{ gitea.repository }}" + - name: Check out repository code + uses: actions/checkout@v4 + - name: Sync repos + uses: Vova-SH/sync-upstream-repo@1.0.5 + with: + upstream_repo: ${{ env.CORE_REPO }} + token: ${{ env.TOKEN }} + spawn_logs: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b3f5536 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "gradle"] + path = gradle + url = https://git.sicampus.ru/core/gradle.git +[submodule "buildSrc"] + path = buildSrc + url = https://git.sicampus.ru/core/dependecies.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef91da2 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +[![Android Studio version](https://img.shields.io/endpoint?url=https%3A%2F%2Fsicampus.ru%2Fgitea%2Fcore%2Fdocs%2Fraw%2Fbranch%2Fmain%2Fandroid-studio-label.json)](https://sicampus.ru/gitea/core/docs/src/branch/main/how-upload-project.md) + +# НТО 2024. II отборочный этап. Командные задани — клиентская часть + +## 📖 Предыстория +В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных. +Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него. + + + +## 📋 Системные требования + +| **Параметр** | **Требование** | +|-----------------------------|---------------------------------------| +| **Минимальная версия Android** | 9.0 (API 28) | +| **Целевая версия Android** | 14 (API 34) | +| **Поддерживаемые устройства** | смартфоны, планшеты | +| **Ориентация экранов** | портретная | +| **Языки** | русский, английский | +| **Разрешения** | доступ к интернету, камера (при необходимости) | + + + +## 📱 Техническое задание +Требуется разработать нативное мобильное приложение, которое будет содержать следующие экраны. + + +### 1. Экран авторизации + +> Данный экран должен быть показан при первом входе в приложение, а также в ситуациях, когда пользователь не зарегистрировался в приложении. + +#### Элементы, которые должны присутствовать на экране: +- Поле ввода (`id/username`), в котором пользователю необходимо ввести свой логин. +- Кнопка входа (`id/login`), по нажатию на которую пользователь авторизуется в системе. +- По умолчанию скрытое (`GONE`) текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +1. В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю. +2. Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной: + - Поле ввода пустое. + - Количество символов менее 3х. + - Логин начинается с цифры. + - Логин содержит символы, отличные от латинского алфавита и цифр. +3. Поле ввода и кнопку должно быть видно при раскрытии клавиатуры. +4. - При нажатии на кнопку входа необходимо проверить, что данный пользователь существует с помощью запроса `api//auth` (подробное описание представлено в техническом задании серверной части). +5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку. +6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан. +7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно. +8. Экран авторизации показывается только в случае, если пользователь неавторизован. + + + + +### 2. Главный экран + +> Данный экран содержит общую информацию о пользователе: +>- ФИО +>- Фото +>- Должность +>- Время последнего входа + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/fullname`), в котором написано имя пользователя. +- Изображение (`id/photo`), на котором отображено фото пользователя. +- Текстовое поле (`id/position`), в котором написана должность пользователя. +- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя. +- Кнопка (`id/logout`) для выхода пользователя из аккаунта. +- Кнопка (`id/refresh`) для обновления данных. +- Кнопка (`id/scan`) для сканирования QR кода. +- По умолчанию скрытое текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. +- Для получения данных необходимо использовать сетевой запрос `/api//info`. +- Формат даты и времени последнего входа пользователя: `yyyy-MM-dd HH:mm` (например: 2024-02-31 08:31). Время необходимо отображать с сервера, без поправок на часовой пояс или локальное смещение. +- При нажатии на кнопку выход все данные (если есть) пользователя должны быть очищены, а приложение должно открыть экран авторизации. +- При нажатии кнопки сканирования необходимо открыть экран сканирования QR кода. +- При нажатии на кнопку обновления данных - необходимо повторно вызывать сетевой запрос для получения актуальных данных. + + + +### 3. Экран сканирования QR-кода + +> Данный экран позволяет отсканировать код на турникете и войти с помощью смартфона. В данном случае данный экран будет уже написан и представлен dам в готовом виде в заготовке. Вам необходимо только подписаться на его результат с помощью **Result API** и обработать считанные данные из QR кода. **Данный экран нельзя модифицировать. Он поставляется как есть.** + + + +### 4. Экран с результатом сканирования QR кода + +> На данном экране необходимо вывести успешность или неуспешность входа с помощью метода QR кода. + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/result`), где содержится текст об успешности или неуспешности входа. +- Кнопка (`id/close`) для закрытия данного экрана. + +#### Требования к компонентам: +- В случае, если результат пришёл пустым или со статусом “Отмена” - необходимо вывести пользователю текст: + *"Вход был отменён/Operation was cancelled"* +- В случае, если данные пришли, то необходимо их отправить на сервер с запросом `api//open`, добавив данные из результата и получить ответ. +- Если сервер ответил успешно - то отображаем текст: + *"Успешно/Success"* +- Если сервер ответил любой ошибкой - то отображаем текст: + *"Что-то пошло не так/Something wrong"* +- Кнопка закрытия всегда открывает главный экран. + + + +## 🛠 Решение + +Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149). + +Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах). + + + +## ✅ Особенности оценивания + +Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения. +В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. +Сервер и приложение тестируются независимо. + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a28d464 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + kotlinAndroid + androidApplication + jetbrainsKotlinSerialization version Version.Kotlin.language + kotlinAnnotationProcessor + id("com.google.dagger.hilt.android").version("2.51.1") +} + +val packageName = "ru.myitschool.work" + +android { + namespace = packageName + compileSdk = Version.Android.Sdk.compile + + defaultConfig { + applicationId = packageName + minSdk = Version.Android.Sdk.min + targetSdk = Version.Android.Sdk.target + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures.viewBinding = true + + compileOptions { + sourceCompatibility = Version.Kotlin.javaSource + targetCompatibility = Version.Kotlin.javaSource + } + + kotlinOptions { + jvmTarget = Version.Kotlin.jvmTarget + } +} + +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/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a986978 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ 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/api/ControllerAPI.java b/app/src/main/java/ru/myitschool/work/api/ControllerAPI.java new file mode 100644 index 0000000..4ec495d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/ControllerAPI.java @@ -0,0 +1,108 @@ +package ru.myitschool.work.api; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.util.Objects; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import ru.myitschool.work.core.Constants; +import ru.myitschool.work.api.entity.Code; +import ru.myitschool.work.api.entity.Employee; + +public class ControllerAPI { + // Главный класс по работе с сервером. В качестве дженерик типа передаётся класс, в котором надо + // имплементировать HandlersResultAPI. Потом нужно прописать в полученных методах работу + // с результатами вызова API. И создав экземпляр этого класса вызвать нужный метод. + // + // Примерно так все должно выглядеть: + // + // public class LoginActivity ... implements HandlersResultAPI { + // private ControllerAPI controllerAPI; + // + // @Override + // protected void onCreate(Bundle savedInstanceState) { + // ... + // controllerAPI = new ControllerAPI(); + // ... + // controllerAPI.verificationLogin("Логин пользователя", LoginActivity.this); + // } + // + // @Override + // public void handlerVerificationLogin(boolean result) { + // // Тут работа с результатами controllerAPI.verificationLogin + // } + // + // @Override + // public void handlerGetEmployee(Employee employee) { } + // + // @Override + // public void handlerVisit(boolean result) { } + // } + // + // Всё это по факту один большой костыль, который придуман, чтобы обойти многопоточность. + + private final RequestsAPI managerAPI; + + public ControllerAPI() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(Constants.SERVER_ADDRESS) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + managerAPI = retrofit.create(RequestsAPI.class); + } + + public void verificationLogin(String login, T handler) { + managerAPI.verificationLogin(login).enqueue(new Callback<>() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + Log.d("Test", "(Верификация логина) - Ответ от сервера: " + response.code()); + handler.handlerVerificationLogin(login, response.code() == 200); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e("Test", "(Верификация логина) - " + Objects.requireNonNull(t.getMessage())); + handler.handlerVerificationLogin(login, false); + } + }); + } + + public void getEmployee(String login, T handler) { + managerAPI.getEmployee(login).enqueue(new Callback<>() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + Log.d("Test", "(Получение пользователя) - Ответ от сервера: Тело - " + response.body() + " Код - " + response.code()); + handler.handlerGetEmployee(response.body()); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e("Test", "(Получение пользователя) - " + Objects.requireNonNull(t.getMessage())); + handler.handlerGetEmployee(null); + } + }); + } + + public void visit(String login, Code code, T handler) throws IOException { + managerAPI.visit(login, code).enqueue(new Callback<>() { + @Override + public void onResponse(Call call, Response response) { + Log.d("Test", "(Проверка кода) - Ответ от сервера: Код - " + response.code()); + handler.handlerVisit(response.code()); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("Test", "(Получение пользователя) - " + Objects.requireNonNull(t.getMessage())); + handler.handlerVisit(400); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/HandlersResultAPI.java b/app/src/main/java/ru/myitschool/work/api/HandlersResultAPI.java new file mode 100644 index 0000000..1d1b3ad --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/HandlersResultAPI.java @@ -0,0 +1,9 @@ +package ru.myitschool.work.api; + +import ru.myitschool.work.api.entity.Employee; + +public interface HandlersResultAPI { + void handlerVerificationLogin(String login, boolean result); + void handlerGetEmployee(Employee employee); + void handlerVisit(int result); +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/RequestsAPI.java b/app/src/main/java/ru/myitschool/work/api/RequestsAPI.java new file mode 100644 index 0000000..e5b3123 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/RequestsAPI.java @@ -0,0 +1,23 @@ +package ru.myitschool.work.api; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.PATCH; +import retrofit2.http.Path; +import ru.myitschool.work.api.entity.Code; +import ru.myitschool.work.api.entity.Employee; + +public interface RequestsAPI { + // Тут будут пути и типы api-запросов. + + // Запрос для проверки пользователя, используется при входе. + @GET("/api/{login}/auth") + Call verificationLogin(@Path("login") String login); + + @GET("/api/{login}/info") + Call getEmployee(@Path("login") String login); + + @PATCH("/api/{login}/open") + Call visit(@Path("login") String login, @Body Code code); +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/entity/Code.java b/app/src/main/java/ru/myitschool/work/api/entity/Code.java new file mode 100644 index 0000000..2337457 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/entity/Code.java @@ -0,0 +1,14 @@ +package ru.myitschool.work.api.entity; + +import kotlinx.serialization.Serializable; + +@Serializable +public class Code { + private long value; + + public Code() { } + public Code(long value) { this.value = value;} + + public long getValue() { return value; } + public void setValue(long value) { this.value = value; } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/entity/Employee.java b/app/src/main/java/ru/myitschool/work/api/entity/Employee.java new file mode 100644 index 0000000..e124c36 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/entity/Employee.java @@ -0,0 +1,87 @@ +package ru.myitschool.work.api.entity; + +import androidx.annotation.NonNull; + +import kotlinx.serialization.Serializable; + + +@Serializable +public class Employee { + private long id; + private String login; + private String name; + private String photo; + private String position; + private String lastVisit; + + public Employee() { + } + + public Employee(long id, String login, String name, String photo, String position, String lastVisit) { + this.id = id; + this.login = login; + this.name = name; + this.photo = photo; + this.position = position; + this.lastVisit = lastVisit; + } + + @NonNull + @Override + public String toString() { + return "id: " + id + + ", login: " + login + + ", name: " + name + + ", photo: " + photo + + ", position: " + position + + ", lastVisit: " + lastVisit; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhoto() { + return photo; + } + + public void setPhoto(String photo) { + this.photo = photo; + } + + public String getPosition() { + return position; + } + + public void setPosition(String position) { + this.position = position; + } + + public String getLastVisit() { + return lastVisit; + } + + public void setLastVisit(String lastVisit) { + this.lastVisit = lastVisit; + } +} \ 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..074c3ea --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.core +// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ +object Constants { + // const val SERVER_ADDRESS = "http://localhost:8090" + const val SERVER_ADDRESS = "http://192.168.0.105:8080" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/MyConstants.kt b/app/src/main/java/ru/myitschool/work/core/MyConstants.kt new file mode 100644 index 0000000..c7015b1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/MyConstants.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.core + +object MyConstants { + const val PREFS_FILE: String = "account" + const val PREF_LOGIN: String = "login" + val DEF_VALUE: Nothing? = null +} \ 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..d144f28 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -0,0 +1,66 @@ +package ru.myitschool.work.ui + +import ru.myitschool.work.ui.result.ResultFragment +import android.os.Bundle +import android.util.Log +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.main.MainDestination +import ru.myitschool.work.ui.main.MainFragment +import ru.myitschool.work.ui.qr.scan.QrScanDestination +import ru.myitschool.work.ui.qr.scan.QrScanFragment +import ru.myitschool.work.ui.result.ResultDestination + +// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! +@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 = MainDestination + ) { + fragment() + fragment() + fragment() + fragment() + } + } + + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onSupportNavigateUp() + } + } + ) + } + + override fun onSupportNavigateUp(): Boolean { + Log.d("Test", "(RootActivity) Сработал метод, отвечающий за кнопку назад.") + + 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..0d53e26 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -0,0 +1,122 @@ +package ru.myitschool.work.ui.login + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.text.Editable +import android.util.Log +import android.view.View +import android.widget.EditText +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.api.ControllerAPI +import ru.myitschool.work.api.HandlersResultAPI +import ru.myitschool.work.api.entity.Employee +import ru.myitschool.work.core.MyConstants +import ru.myitschool.work.databinding.FragmentLoginBinding +import java.util.regex.Pattern + +@AndroidEntryPoint +class LoginFragment : Fragment(R.layout.fragment_login), HandlersResultAPI { + private var _binding: FragmentLoginBinding? = null + private val binding: FragmentLoginBinding get() = _binding!! + + private val viewModel: LoginViewModel by viewModels() + + private var controllerAPI: ControllerAPI? = null // Работа с API + private var settings: SharedPreferences? = null // Настройки приложения. Нужны для сохранения логина при входе. + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentLoginBinding.bind(view) + + // ТЗ: + // >>> ✅ В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю. + // >>> ✅ Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной: + // ✅ Поле ввода пустое. + // ✅ Количество символов менее 3-х. + // ✅ Логин начинается с цифры. + // ✅ Логин содержит символы, отличные от латинского алфавита и цифр. + // >>> ✅ Поле ввода и кнопку должно быть видно при раскрытии клавиатуры. + // >>> ✅ При нажатии на кнопку входа необходимо проверить, что данный пользователь существует + // с помощью запроса api//auth. + // >>> ✅ В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, + // пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку. + // >>> ✅ После нажатия на кнопку - логин должен быть сохранён и при следующем открытии + // приложения экран авторизации не должен быть показан. + // >>> ✅ После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не + // должен быть показан повторно. + // >>> ✅ Экран авторизации показывается только в случае, если пользователь не авторизован. +// usernameEditText = findViewById(R.id.username) +// loginButton = findViewById