From a7ba596d97152bf643911e318b25a970d5264b63 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 18 Feb 2025 19:40:34 +0300 Subject: [PATCH] Initial commit --- .gitea/workflows/automerge-with-core.yaml | 26 ++ .gitignore | 10 + .gitmodules | 6 + README.md | 121 +++++++++ app/.gitignore | 1 + app/build.gradle.kts | 69 ++++++ app/proguard-rules.pro | 21 ++ app/src/main/AndroidManifest.xml | 33 +++ app/src/main/java/ru/myitschool/work/App.kt | 7 + .../ru/myitschool/work/api/ControllerAPI.java | 108 ++++++++ .../work/api/HandlersResultAPI.java | 9 + .../ru/myitschool/work/api/RequestsAPI.java | 23 ++ .../ru/myitschool/work/api/entity/Code.java | 14 ++ .../myitschool/work/api/entity/Employee.java | 87 +++++++ .../java/ru/myitschool/work/core/Constants.kt | 6 + .../ru/myitschool/work/core/MyConstants.kt | 7 + .../ru/myitschool/work/ui/RootActivity.kt | 66 +++++ .../work/ui/login/LoginDestination.kt | 6 + .../myitschool/work/ui/login/LoginFragment.kt | 122 +++++++++ .../work/ui/login/LoginViewModel.kt | 17 ++ .../ui/login/UsernameEditTextListener.java | 24 ++ .../work/ui/main/MainDestination.kt | 6 + .../myitschool/work/ui/main/MainFragment.kt | 184 ++++++++++++++ .../work/ui/qr/scan/QrScanDestination.kt | 30 +++ .../work/ui/qr/scan/QrScanFragment.kt | 139 +++++++++++ .../work/ui/qr/scan/QrScanViewModel.kt | 93 +++++++ .../work/ui/result/ResultDestination.kt | 6 + .../work/ui/result/ResultFragment.kt | 75 ++++++ .../myitschool/work/utils/DownloadImage.java | 37 +++ .../myitschool/work/utils/FlowExtensions.kt | 10 + .../work/utils/FragmentExtesions.kt | 18 ++ .../work/utils/TextChangedListener.kt | 12 + .../myitschool/work/utils/ViewExtensions.kt | 7 + app/src/main/res/drawable/ic_close.xml | 5 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 +++ app/src/main/res/drawable/ic_logout.xml | 5 + app/src/main/res/drawable/ic_no_img.xml | 5 + app/src/main/res/drawable/ic_qr_code.xml | 25 ++ app/src/main/res/drawable/ic_refresh.xml | 5 + .../main/res/layout-w936dp/activity_root.xml | 15 ++ .../main/res/layout-w936dp/fragment_login.xml | 54 ++++ .../main/res/layout-w936dp/fragment_main.xml | 122 +++++++++ .../res/layout-w936dp/fragment_result.xml | 27 ++ app/src/main/res/layout/activity_root.xml | 15 ++ app/src/main/res/layout/fragment_login.xml | 54 ++++ app/src/main/res/layout/fragment_main.xml | 115 +++++++++ app/src/main/res/layout/fragment_qr_scan.xml | 35 +++ app/src/main/res/layout/fragment_result.xml | 26 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.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 app/src/main/res/values-en/strings.xml | 23 ++ app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/ids.xml | 4 + app/src/main/res/values/strings.xml | 22 ++ app/src/main/res/values/strings_qr.xml | 4 + app/src/main/res/values/themes.xml | 16 ++ app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 4 + build.gradle.kts | 7 + buildSrc/.gitignore | 2 + buildSrc/build.gradle.kts | 8 + buildSrc/src/main/java/Dependencies.kt | 220 ++++++++++++++++ .../main/java/DependencyHandlerExtensions.kt | 40 +++ buildSrc/src/main/java/Plugin.kt | 67 +++++ buildSrc/src/main/java/Version.kt | 42 ++++ gradle.properties | 21 ++ gradle/README.md | 11 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++++++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle.kts | 17 ++ 84 files changed, 2999 insertions(+) create mode 100644 .gitea/workflows/automerge-with-core.yaml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/ru/myitschool/work/App.kt create mode 100644 app/src/main/java/ru/myitschool/work/api/ControllerAPI.java create mode 100644 app/src/main/java/ru/myitschool/work/api/HandlersResultAPI.java create mode 100644 app/src/main/java/ru/myitschool/work/api/RequestsAPI.java create mode 100644 app/src/main/java/ru/myitschool/work/api/entity/Code.java create mode 100644 app/src/main/java/ru/myitschool/work/api/entity/Employee.java create mode 100644 app/src/main/java/ru/myitschool/work/core/Constants.kt create mode 100644 app/src/main/java/ru/myitschool/work/core/MyConstants.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/RootActivity.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/UsernameEditTextListener.java create mode 100644 app/src/main/java/ru/myitschool/work/ui/main/MainDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/main/MainFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/result/ResultDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/result/ResultFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/DownloadImage.java create mode 100644 app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_logout.xml create mode 100644 app/src/main/res/drawable/ic_no_img.xml create mode 100644 app/src/main/res/drawable/ic_qr_code.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/layout-w936dp/activity_root.xml create mode 100644 app/src/main/res/layout-w936dp/fragment_login.xml create mode 100644 app/src/main/res/layout-w936dp/fragment_main.xml create mode 100644 app/src/main/res/layout-w936dp/fragment_result.xml create mode 100644 app/src/main/res/layout/activity_root.xml create mode 100644 app/src/main/res/layout/fragment_login.xml create mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/fragment_qr_scan.xml create mode 100644 app/src/main/res/layout/fragment_result.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-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/values-en/strings.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/strings_qr.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/main/res/xml/network_security_config.xml create mode 100644 build.gradle.kts create mode 100644 buildSrc/.gitignore create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/java/Dependencies.kt create mode 100644 buildSrc/src/main/java/DependencyHandlerExtensions.kt create mode 100644 buildSrc/src/main/java/Plugin.kt create mode 100644 buildSrc/src/main/java/Version.kt create mode 100644 gradle.properties create mode 100644 gradle/README.md create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts 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