Initial commit

This commit is contained in:
Andrey 2025-02-18 19:40:34 +03:00
commit a7ba596d97
84 changed files with 2999 additions and 0 deletions

View File

@ -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

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

6
.gitmodules vendored Normal file
View File

@ -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

121
README.md Normal file
View File

@ -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/<LOGIN>/auth` (подробное описание представлено в техническом задании серверной части).
5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку.
6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан.
7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно.
8. Экран авторизации показывается только в случае, если пользователь неавторизован.
### 2. Главный экран
> Данный экран содержит общую информацию о пользователе:
>- ФИО
>- Фото
>- Должность
>- Время последнего входа
#### Элементы, которые должны присутствовать на экране:
- Текстовое поле (`id/fullname`), в котором написано имя пользователя.
- Изображение (`id/photo`), на котором отображено фото пользователя.
- Текстовое поле (`id/position`), в котором написана должность пользователя.
- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя.
- Кнопка (`id/logout`) для выхода пользователя из аккаунта.
- Кнопка (`id/refresh`) для обновления данных.
- Кнопка (`id/scan`) для сканирования QR кода.
- По умолчанию скрытое текстовое поле с ошибкой (`id/error`).
#### Требования к компонентам:
- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных.
- Для получения данных необходимо использовать сетевой запрос `/api/<LOGIN>/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/<LOGIN>/open`, добавив данные из результата и получить ответ.
- Если сервер ответил успешно - то отображаем текст:
*"Успешно/Success"*
- Если сервер ответил любой ошибкой - то отображаем текст:
*"Что-то пошло не так/Something wrong"*
- Кнопка закрытия всегда открывает главный экран.
## 🛠 Решение
Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149).
Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах).
## ✅ Особенности оценивания
Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения.
В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы.
Сервер и приложение тестируются независимо.

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

69
app/build.gradle.kts Normal file
View File

@ -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
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Default"
tools:targetApi="31">
<activity
android:name=".ui.RootActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,7 @@
package ru.myitschool.work
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application()

View File

@ -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<T extends HandlersResultAPI> {
// Главный класс по работе с сервером. В качестве дженерик типа передаётся класс, в котором надо
// имплементировать HandlersResultAPI. Потом нужно прописать в полученных методах работу
// с результатами вызова API. И создав экземпляр этого класса вызвать нужный метод.
//
// Примерно так все должно выглядеть:
//
// public class LoginActivity ... implements HandlersResultAPI {
// private ControllerAPI controllerAPI;
//
// @Override
// protected void onCreate(Bundle savedInstanceState) {
// ...
// controllerAPI = new ControllerAPI<LoginActivity>();
// ...
// 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<Void> call, @NonNull Response<Void> response) {
Log.d("Test", "(Верификация логина) - Ответ от сервера: " + response.code());
handler.handlerVerificationLogin(login, response.code() == 200);
}
@Override
public void onFailure(@NonNull Call<Void> 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<Employee> call, @NonNull Response<Employee> response) {
Log.d("Test", "(Получение пользователя) - Ответ от сервера: Тело - " + response.body() + " Код - " + response.code());
handler.handlerGetEmployee(response.body());
}
@Override
public void onFailure(@NonNull Call<Employee> 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<Void> call, Response<Void> response) {
Log.d("Test", "(Проверка кода) - Ответ от сервера: Код - " + response.code());
handler.handlerVisit(response.code());
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Log.e("Test", "(Получение пользователя) - " + Objects.requireNonNull(t.getMessage()));
handler.handlerVisit(400);
}
});
}
}

View File

@ -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);
}

View File

@ -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<Void> verificationLogin(@Path("login") String login);
@GET("/api/{login}/info")
Call<Employee> getEmployee(@Path("login") String login);
@PATCH("/api/{login}/open")
Call<Void> visit(@Path("login") String login, @Body Code code);
}

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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<LoginFragment, LoginDestination>()
fragment<MainFragment, MainDestination>()
fragment<QrScanFragment, QrScanDestination>()
fragment<ResultFragment, ResultDestination>()
}
}
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()
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.login
import kotlinx.serialization.Serializable
@Serializable
data object LoginDestination

View File

@ -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<LoginFragment>? = null // Работа с API
private var settings: SharedPreferences? = null // Настройки приложения. Нужны для сохранения логина при входе.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentLoginBinding.bind(view)
// ТЗ:
// >>> ✅ В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю.
// >>> ✅ Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной:
// ✅ Поле ввода пустое.
// ✅ Количество символов менее 3-х.
// ✅ Логин начинается с цифры.
// ✅ Логин содержит символы, отличные от латинского алфавита и цифр.
// >>> ✅ Поле ввода и кнопку должно быть видно при раскрытии клавиатуры.
// >>> ✅ При нажатии на кнопку входа необходимо проверить, что данный пользователь существует
// с помощью запроса api/<LOGIN>/auth.
// >>> ✅ В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку,
// пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку.
// >>> ✅ После нажатия на кнопку - логин должен быть сохранён и при следующем открытии
// приложения экран авторизации не должен быть показан.
// >>> ✅ После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не
// должен быть показан повторно.
// >>> ✅ Экран авторизации показывается только в случае, если пользователь не авторизован.
// usernameEditText = findViewById<EditText>(R.id.username)
// loginButton = findViewById<Button>(R.id.login)
// errorTextView = findViewById<TextView>(R.id.error)
controllerAPI = ControllerAPI<LoginFragment>()
settings = this.requireActivity().getSharedPreferences(MyConstants.PREFS_FILE, Context.MODE_PRIVATE)
// Этот код отвечает за прослушивание ввода логина. Нужен для 2 задания из ТЗ.
// Он использует абстрактный класс UsernameEditTextListener, который просто сокращает TextWatcher.
binding.username.addTextChangedListener(object : UsernameEditTextListener<EditText>(binding.username) {
override fun onTextChanged(target: EditText, s: Editable) {
val userLogin = s.toString()
val pattern = Pattern.compile("[^a-zA-Z0-9]")
val result = !(userLogin.isEmpty() || userLogin.length < 3 ||
Character.isDigit(userLogin[0]) ||
pattern.matcher(userLogin).find())
binding.login.setEnabled(result)
// ТЗ: Необходимо вывести ошибку, пока пользователь не изменит текстовое поле
binding.error.visibility = View.GONE
}
})
// Прослушиваем нажатие на кнопку `Вход`.
binding.login.setOnClickListener {
controllerAPI!!.verificationLogin(binding.username.text.toString(), this)
}
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun handlerVerificationLogin(login: String?, result: Boolean) {
binding.error.visibility = View.GONE // Тз: необходимо вывести ошибку, пока пользователь
// не изменит текстовое поле или повторно не нажмёт на кнопку.
if (result) {
// Этот код отвечает за сохранения факта входа.
// Код копировал отсюда -> https://metanit.com/java/android/12.1.php
val prefEditor = settings!!.edit()
prefEditor.putString(MyConstants.PREF_LOGIN, login)
prefEditor.apply()
Log.d(
"Test",
"(LoginActivity) Проверка сохранения логина: " + settings!!.getString(
MyConstants.PREF_LOGIN,
"Не сохранилось"
)
)
Log.d("Test", "(LoginActivity) Начало перехода на главную активность.")
// findNavController().navigate(MainDestination)
findNavController().popBackStack()
} else {
Log.d("Test", "(LoginActivity) Введён неправильный логин! login: $login")
binding.error.visibility = View.VISIBLE
}
}
override fun handlerGetEmployee(employee: Employee?) {
}
override fun handlerVisit(result: Int) {
}
}

View File

@ -0,0 +1,17 @@
package ru.myitschool.work.ui.login
import android.content.Context
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
@ApplicationContext private val context: Context,
) : ViewModel() {
private val _state = MutableStateFlow(true)
val state = _state.asStateFlow()
}

View File

@ -0,0 +1,24 @@
package ru.myitschool.work.ui.login;
import android.text.Editable;
import android.text.TextWatcher;
public abstract class UsernameEditTextListener<T> implements TextWatcher {
// Нужен, чтобы убрать ненужные мне методы beforeTextChanged и onTextChanged.
// https://stackoverflow.com/questions/11134144/android-edittext-onchange-listener
private final T target;
public UsernameEditTextListener(T target) { this.target = target; }
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) { this.onTextChanged(target, s); }
public abstract void onTextChanged(T target, Editable s);
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.main
import kotlinx.serialization.Serializable
@Serializable
data object MainDestination

View File

@ -0,0 +1,184 @@
package ru.myitschool.work.ui.main
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity.MODE_PRIVATE
import androidx.fragment.app.Fragment
import androidx.fragment.app.clearFragmentResultListener
import androidx.fragment.app.setFragmentResultListener
import androidx.navigation.fragment.findNavController
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.FragmentMainBinding
import ru.myitschool.work.ui.login.LoginDestination
import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.ui.result.ResultDestination
import ru.myitschool.work.utils.DownloadImage
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class MainFragment : Fragment(R.layout.fragment_main), HandlersResultAPI {
private var _binding: FragmentMainBinding? = null
private val binding: FragmentMainBinding get() = _binding!!
private lateinit var controllerAPI: ControllerAPI<MainFragment>
private lateinit var settings: SharedPreferences
private lateinit var login: String
private var flag = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentMainBinding.bind(view)
controllerAPI = ControllerAPI<MainFragment>()
settings = this.requireActivity().getSharedPreferences(MyConstants.PREFS_FILE, MODE_PRIVATE)
if (flag && savedInstanceState == null) {
Log.d("Test", "(1)")
val editor = settings.edit()
editor.putString("code", "Отмена")
editor.putString("login", login)
editor.apply()
flag = false
findNavController().navigate(ResultDestination)
clearFragmentResultListener("qr_result")
}
init()
}
private fun init() {
Log.d("Test", "(MainFragment) Проверяем зарегистрирован пользователь или нет.")
if (settings.getString(MyConstants.PREF_LOGIN, MyConstants.DEF_VALUE) == MyConstants.DEF_VALUE) {
Log.d("Test", "(MainFragment) Пользователь не зарегистрирован, запускаем активность регистрации.")
findNavController().navigate(LoginDestination)
return
}
login = settings.getString(MyConstants.PREF_LOGIN, MyConstants.DEF_VALUE).toString()
Log.d("Test", "(MainFragment) Пользователь зарегистрирован, его login - $login")
Log.d("Test", "(MainFragment) Отправляем запрос на получение данных пользователя.")
controllerAPI.getEmployee(login, this)
binding.logout.setOnClickListener {
Log.d("Test", "(MainFragment) Запущен обработчик кнопки `Выход`.")
settings.edit().clear().apply()
Log.d("Test",
"(MainFragment) Теперь логин пользователя: " + settings.getString(
MyConstants.PREF_LOGIN,
MyConstants.DEF_VALUE
).toString()
)
findNavController().navigate(LoginDestination)
}
binding.refresh.setOnClickListener {
Log.d("Test", "(MainFragment) Запущен обработчик кнопки `Обновить`.")
Log.d("Test", "(MainFragment) Отправляем запрос на получение данных пользователя.")
controllerAPI.getEmployee(login, this)
}
binding.scan.setOnClickListener {
setFragmentResultListener("qr_result") { _, bundle ->
Log.d("Test", bundle.toString())
val code: String = QrScanDestination.getDataIfExist(bundle) ?: return@setFragmentResultListener
val editor = settings.edit()
editor.putString("code", code)
editor.putString("login", login)
editor.apply()
Log.d("Test", "(2)")
flag = false
findNavController().navigate(ResultDestination)
clearFragmentResultListener("qr_result")
}
flag = true
findNavController().navigate(QrScanDestination)
}
}
override fun handlerVerificationLogin(login: String?, result: Boolean) {
}
@RequiresApi(Build.VERSION_CODES.O)
override fun handlerGetEmployee(employee: Employee?) {
if (employee == null) {
Log.e("Test", "(MainFragment) Сервер не вернул работника, вероятно, он не запущен.")
allGone()
binding.error.visibility = View.VISIBLE
binding.refresh.visibility = View.VISIBLE
return
}
Log.d("Test", "(MainFragment) Обработчик получения данных работника запустился!")
Log.d("Test", "(MainFragment) employee: $employee")
allVisibility()
binding.error.visibility = View.GONE
binding.fullname.text = employee.name
binding.position.text = employee.position
// Форматируем дату и время в вид yyyy-MM-dd HH:mm
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val dateTime = LocalDateTime.parse(employee.lastVisit)
val lastVisit = dateTime.format(formatter)
binding.lastEntry.text = lastVisit
downloadImage(employee.photo)
Log.d(
"Test", "(MainFragment) Записанные данные: fullname: ${binding.fullname.text}, " +
"position: ${binding.position.text}, lastEntry: ${binding.lastEntry.text}"
)
}
override fun handlerVisit(result: Int) {
// Тут ничего не должно быть.
}
private fun allGone() {
binding.refresh.visibility = View.GONE
binding.scan.visibility = View.GONE
binding.logout.visibility = View.GONE
binding.error.visibility = View.GONE
binding.position.visibility = View.GONE
binding.lastEntry.visibility = View.GONE
binding.fullname.visibility = View.GONE
binding.photo.visibility = View.GONE
}
private fun allVisibility() {
binding.refresh.visibility = View.VISIBLE
binding.scan.visibility = View.VISIBLE
binding.logout.visibility = View.VISIBLE
binding.error.visibility = View.VISIBLE
binding.position.visibility = View.VISIBLE
binding.lastEntry.visibility = View.VISIBLE
binding.fullname.visibility = View.VISIBLE
binding.photo.visibility = View.VISIBLE
}
private fun downloadImage(urlImage: String) {
Log.d("Test", "(MainFragment) Начинаем скачивать изображение с URl: $urlImage")
DownloadImage(binding.photo).execute(urlImage)
}
}

View File

@ -0,0 +1,30 @@
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
)
}
}

View File

@ -0,0 +1,139 @@
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
}
}
}

View File

@ -0,0 +1,93 @@
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<Action>()
val action = _action.asSharedFlow()
private val _state = MutableStateFlow<State>(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
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.result
import kotlinx.serialization.Serializable
@Serializable
data object ResultDestination

View File

@ -0,0 +1,75 @@
package ru.myitschool.work.ui.result
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity.MODE_PRIVATE
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R
import ru.myitschool.work.api.ControllerAPI
import ru.myitschool.work.api.HandlersResultAPI
import ru.myitschool.work.api.entity.Code
import ru.myitschool.work.api.entity.Employee
import ru.myitschool.work.core.MyConstants
import ru.myitschool.work.databinding.FragmentResultBinding
import ru.myitschool.work.ui.main.MainDestination
import java.io.IOException
class ResultFragment : Fragment(R.layout.fragment_result), HandlersResultAPI {
private var _binding: FragmentResultBinding? = null
private val binding: FragmentResultBinding get() = _binding!!
private lateinit var controllerAPI: ControllerAPI<ResultFragment> // Работа с API
private lateinit var settings: SharedPreferences
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentResultBinding.bind(view)
controllerAPI = ControllerAPI<ResultFragment>()
settings = this.requireActivity().getSharedPreferences(MyConstants.PREFS_FILE, MODE_PRIVATE)
val code = settings.getString("code", "").toString()
val login = settings.getString("login", "").toString()
Log.d("Test", "(ResultLayoutActivity) code: $code, login: $login")
if (code == "Отмена" || code == "") {
binding.result.setText(R.string.cancelled)
} else {
try {
controllerAPI.visit(login, Code(code.toLong()), this)
} catch (e: NumberFormatException) {
binding.result.setText(R.string.wrong)
} catch (e: IOException) {
binding.result.setText(R.string.wrong)
}
}
binding.close.setOnClickListener {
findNavController().navigate(MainDestination)
}
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun handlerVerificationLogin(login: String?, result: Boolean) {
}
override fun handlerGetEmployee(employee: Employee?) {
}
override fun handlerVisit(result: Int) {
if (result == 200) {
binding.result.setText(R.string.success)
} else {
binding.result.setText(R.string.wrong)
}
}
}

View File

@ -0,0 +1,37 @@
package ru.myitschool.work.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import java.io.InputStream;
import java.net.URL;
public class DownloadImage extends AsyncTask<String, Void, Bitmap> {
ImageView imageView;
public DownloadImage(ImageView imageView) {
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(String... imageUrls) {
String imageUrl = imageUrls[0];
try {
InputStream inputStream = new URL(imageUrl).openStream();
return BitmapFactory.decodeStream(inputStream);
} catch (Exception e) {
Log.e("Test", "(DownloadImage) При скачивании изображения произошла ошибка: " + e);
return null;
}
}
protected void onPostExecute(Bitmap result) {
Log.d("Test", "(DownloadImage) Устанавливаем изображение в imageView (" + imageView.getId() + ")");
imageView.setImageBitmap(result);
}
}

View File

@ -0,0 +1,10 @@
package ru.myitschool.work.utils
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
fun <T> MutablePublishFlow() = MutableSharedFlow<T>(
replay = 0,
extraBufferCapacity = 1,
BufferOverflow.DROP_OLDEST
)

View File

@ -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 <T> Flow<T>.collectWhenStarted(
fragment: Fragment,
crossinline collector: (T) -> Unit
) {
fragment.viewLifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value ->
collector.invoke(value)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M21.9,21.9l-8.49,-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1 0.9,2 2,2h13.17l2.31,2.31L21.9,21.9zM5,18l3.5,-4.5l2.5,3.01L12.17,15l3,3H5zM21,18.17L5.83,3H19c1.1,0 2,0.9 2,2V18.17z"/>
</vector>

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
<path android:fillColor="@android:color/white" android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/>
<path android:fillColor="@android:color/white" android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,19h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M13,13h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M15,15h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M13,17h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M15,19h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M17,17h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M17,13h2v2h-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,15h2v2h-2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</FrameLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/login_activity"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:gravity="center"
android:text="@string/error_message_login_activity"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="gone"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:padding="5dp" />
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="48dp"
android:autofillHints="text"
android:hint="@string/prompt_username"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
android:textColor="#000000"
android:textSize="20sp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="150dp" />
<Button
android:id="@+id/login"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/action_sign_in"
android:textSize="20sp"
android:layout_below="@+id/username"
android:layout_centerHorizontal="true"
android:layout_marginTop="80dp" />
</RelativeLayout>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_fragment">
<ImageView
android:id="@+id/photo"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@string/content_description" />
<TextView
android:id="@+id/fullname"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center"
android:textSize="26sp" />
<TextView
android:id="@+id/position"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_below="@+id/fullname"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center"
android:textSize="26sp" />
<TextView
android:id="@+id/lastEntry"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_below="@+id/position"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center"
android:textSize="26sp" />
<Button
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/lastEntry"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:text="@string/refresh"
android:textSize="26sp" />
<Button
android:id="@+id/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/photo"
android:layout_alignEnd="@+id/photo"
android:layout_alignParentStart="true"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:text="@string/logout_text"
android:textSize="26sp" />
<Button
android:id="@+id/scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/logout"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/logout"
android:text="@string/scan_text"
android:textSize="26sp" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:gravity="center"
android:text="@string/error_message_root_activity"
android:textAlignment="center"
android:visibility="gone"
android:padding="5dp"
android:textSize="26sp" />
</RelativeLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:screenOrientation="portrait" >
<TextView
android:id="@+id/result"
android:layout_width="570dp"
android:layout_height="305dp"
android:layout_above="@+id/close"
android:layout_centerHorizontal="true"
android:layout_marginBottom="100dp"
android:padding="5dp"
android:textSize="30sp" />
<Button
android:id="@+id/close"
android:layout_width="533dp"
android:layout_height="76dp"
android:layout_centerInParent="true"
android:text="@string/close"
android:textSize="30sp" />
</RelativeLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</FrameLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_activity"
android:screenOrientation="portrait"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:text="@string/error_message_login_activity"
android:gravity="center"
android:textAlignment="center"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:visibility="gone"
android:padding="5dp" />
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="48dp"
android:hint="@string/prompt_username"
android:inputType="textEmailAddress"
android:autofillHints="text"
android:selectAllOnFocus="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="96dp" />
<Button
android:id="@+id/login"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/action_sign_in"
android:layout_below="@+id/username"
android:layout_marginTop="40dp"
android:layout_centerHorizontal="true" />
</RelativeLayout>

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_fragment">
<ImageView
android:id="@+id/photo"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@string/content_description" />
<TextView
android:id="@+id/fullname"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center" />
<TextView
android:id="@+id/position"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/fullname"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center" />
<TextView
android:id="@+id/lastEntry"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_below="@+id/position"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:gravity="center" />
<Button
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/lastEntry"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/photo"
android:text="@string/refresh" />
<Button
android:id="@+id/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/photo"
android:layout_alignEnd="@+id/photo"
android:layout_alignParentStart="true"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:text="@string/logout_text" />
<Button
android:id="@+id/scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/logout"
android:layout_alignParentEnd="true"
android:layout_marginStart="30dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/logout"
android:text="@string/scan_text" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:gravity="center"
android:text="@string/error_message_root_activity"
android:textAlignment="center"
android:visibility="gone"
android:padding="5dp" />
</RelativeLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/close_button"
android:src="@drawable/ic_close"
app:elevation="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_result"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/result"
android:layout_width="180dp"
android:layout_height="80dp"
android:layout_above="@+id/close"
android:layout_centerHorizontal="true"
android:layout_marginBottom="100dp"
android:padding="5dp" />
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/close" />
</RelativeLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NTO Client 2024</string>
<!-- Всё, что относится к LoginActivity -->
<string name="title_activity_login">LoginActivity</string>
<string name="prompt_username">Login</string>
<string name="action_sign_in">Sing In</string>
<string name="action_sign_in_short">Sign in</string>
<string name="error_message_login_activity">Such a user has not been found!\nTry logging in with a different username.</string>
<!-- Всё, что относится к RootActivity -->
<string name="error_message_root_activity">An error has occurred, please update the data.</string>
<string name="scan_text">Scan</string>
<string name="logout_text">Exit</string>
<string name="refresh">Update</string>
<string name="content_description">Profile Photo</string>
<string name="close">Close</string>
<string name="cancelled">Operation was cancelled</string>
<string name="success">Success</string>
<string name="wrong">Something wrong</string>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="rootContainer" type="id"/>
</resources>

View File

@ -0,0 +1,22 @@
<resources>
<string name="app_name">NTO Client 2024</string>
<!-- Всё, что относится к LoginActivity -->
<string name="title_activity_login">LoginActivity</string>
<string name="prompt_username">Логин</string>
<string name="action_sign_in">Вход</string>
<string name="action_sign_in_short">Sign in</string>
<string name="error_message_login_activity">Такой пользователь не найден!\nПопробуйте с войти с другим логином.</string>
<!-- Всё, что относится к RootActivity -->
<string name="error_message_root_activity">Возникла ошибка, пожалуйста, обновите данные.</string>
<string name="scan_text">Сканировать</string>
<string name="logout_text">Выход</string>
<string name="refresh">Обновить</string>
<string name="content_description">Фото профиля</string>
<string name="close">Закрыть</string>
<string name="cancelled">Вход был отменён</string>
<string name="success">Успешно</string>
<string name="wrong">Что-то пошло не так</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="close_button">Close</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Default" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true"/>
</network-security-config>

7
build.gradle.kts Normal file
View File

@ -0,0 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
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
}

2
buildSrc/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.gradle
/build

View File

@ -0,0 +1,8 @@
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}

View File

@ -0,0 +1,220 @@
data class Dependency(
val name: String,
val version: String,
) {
val fullPath get() = "$name:$version"
}
object Dependencies {
/**
* Type-safe HTTP client for Android and Java by Square, Inc.
*
* [Documentation](http://square.github.io/retrofit/)
*
* [Github](https://github.com/square/retrofit)
*
* [Apache License 2.0](https://github.com/square/retrofit/blob/master/LICENSE.txt)
*
* [Changelog](https://github.com/square/retrofit/blob/master/CHANGELOG.md)
*/
object Retrofit {
private const val version = "2.9.0"
val library = Dependency("com.squareup.retrofit2:retrofit", version)
val gsonConverter = Dependency("com.squareup.retrofit2:converter-gson", version)
}
/**
* [Documentation](https://developer.android.com/jetpack/androidx)
*
* [Releases](https://developer.android.com/jetpack/androidx/versions).
*/
object AndroidX {
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/core)
*/
val core = Dependency("androidx.core:core-ktx", "1.13.1")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/appcompat)
*/
val appcompat = Dependency("androidx.appcompat:appcompat", "1.7.0")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/activity)
*/
val activity = Dependency("androidx.activity:activity", "1.9.3")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/fragment)
*/
val fragment = Dependency("androidx.fragment:fragment-ktx", "1.8.4")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/recyclerview)
*/
val recyclerView = Dependency("androidx.recyclerview:recyclerview", "1.3.2")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/cardview)
*/
val cardView = Dependency("androidx.cardview:cardview", "1.0.0")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/gridlayout)
*/
val gridLayout = Dependency("androidx.gridlayout:gridlayout", "1.0.0")
/**
* A ConstraintLayout is a ViewGroup which allows you to position and size widgets in a flexible way.
*
* [Documentation](https://developer.android.com/reference/android/support/constraint/ConstraintLayout)
*
* [Changelog](https://developer.android.com/jetpack/androidx/releases/constraintlayout)
*/
val constraintLayout = Dependency("androidx.constraintlayout:constraintlayout", "2.1.4")
/**
* CoordinatorLayout is a super-powered FrameLayout.
* CoordinatorLayout is intended for two primary use cases:
* 1. As a top-level application decor or chrome layout
* 2. As a container for a specific interaction with one or more child views
*
* [Documentation](https://developer.android.com/jetpack/androidx/releases/coordinatorlayout)
*
* [Changelog](https://developer.android.com/jetpack/androidx/releases/coordinatorlayout)
*/
val coordinatorLayout = Dependency("androidx.coordinatorlayout:coordinatorlayout", "1.2.0")
/**
* The SwipeRefreshLayout should be used whenever the user
* can refresh the contents of a view via a vertical swipe gesture.
*
* [Documentation](https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout)
*
* [Changelog](https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout)
*/
val swipeRefreshLayout = Dependency("androidx.swiperefreshlayout:swiperefreshlayout", "1.1.0")
/**
* [Changelog](https://developer.android.com/jetpack/androidx/releases/test/)
*/
object Testing {
/**
* [Documentation](https://developer.android.com/training/testing)
*/
val core = Dependency("androidx.test:core", "1.5.0")
/**
* [Documentation](https://developer.android.com/training/testing)
*/
val junit = Dependency("androidx.test.ext:junit-ktx", "1.1.5")
/**
* [Documentation](https://developer.android.com/training/testing/espresso)
*/
object Espresso {
private const val version = "3.5.1"
val core = Dependency("androidx.test.espresso:espresso-core", version)
val intents = Dependency("androidx.test.espresso:espresso-intents", version)
val contrib = Dependency("androidx.test.espresso:espresso-contrib", version)
}
/**
* [Documentation](https://developer.android.com/training/testing/junit-runner)
*/
val runner = Dependency("androidx.test:runner", "1.5.2")
/**
* [Documentation](https://developer.android.com/training/testing/junit-rules)
*/
val rules = Dependency("androidx.test:rules", "1.5.0")
/**
* [Documentation](https://developer.android.com/training/testing/junit-rules)
*/
val compose = Dependency("androidx.compose.ui:ui-test-junit4", "1.6.0")
/**
* [Documentation](https://developer.android.com/training/testing/junit-runner#using-android-test-orchestrator)
*/
val orchestrator = Dependency("androidx.test:orchestrator", "1.4.2")
/**
* [Documentation](https://developer.android.com/training/testing/ui-automator)
*/
val uiAutomator = Dependency("androidx.test.uiautomator:uiautomator", "2.2.0")
}
/**
* [Documentation](https://material.io/develop/android/)
*
* [Github](https://github.com/material-components/material-components-android)
*
* [Changelog](https://github.com/material-components/material-components-android/releases)
*/
val materialDesign = Dependency("com.google.android.material:material", "1.11.0")
object Lifecycle {
private const val version = "2.6.1"
val viewModel = Dependency("androidx.lifecycle:lifecycle-viewmodel-ktx", version)
val common = Dependency("androidx.lifecycle:lifecycle-common", version)
}
object Navigation {
private const val version = "2.8.3"
val fragment = Dependency("androidx.navigation:navigation-fragment-ktx", version)
val navigationUi = Dependency("androidx.navigation:navigation-ui-ktx", version)
}
}
/**
* JUnit is a simple framework to write repeatable tests.
*
* [Documentation](https://junit.org/junit4/)
*
* [Github](https://github.com/junit-team/junit4)
*
* [Eclipse Public License 1.0](https://github.com/junit-team/junit4/blob/master/LICENSE-junit.txt)
*
* [Changelog](https://github.com/junit-team/junit4/wiki)
*/
val junit = Dependency("junit:junit", "4.13")
/**
* Truth makes your test assertions and failure messages more readable.
* Similar to AssertJ, it natively supports many JDK and Guava types,
* and it is extensible to others.
*
* [Documentation](https://truth.dev/)
*
* [Github](https://github.com/google/truth)
*
* [Apache License 2.0](https://github.com/google/truth/blob/master/LICENSE)
*
* [Changelog](https://github.com/google/truth/releases)
*/
val truth = Dependency("com.google.truth:truth", "1.3.0")
/**
* Kaspresso is a framework for Android UI testing. Based on Espresso and UI Automator.
*
* [Documentation](https://kasperskylab.github.io/Kaspresso/)
*
* [Github](https://github.com/KasperskyLab/Kaspresso)
*
* [Apache License 2.0](https://github.com/KasperskyLab/Kaspresso/blob/master/LICENSE.txt)
*
* [Changelog](https://github.com/KasperskyLab/Kaspresso/releases)
*/
object Kaspresso {
private const val version = "1.5.3"
val core = Dependency("com.kaspersky.android-components:kaspresso", version)
val composeSupport = Dependency("com.kaspersky.android-components:kaspresso-compose-support", version)
}
}

View File

@ -0,0 +1,40 @@
import org.gradle.api.artifacts.dsl.DependencyHandler
fun DependencyHandler.implementation(dependency: Dependency) {
add(Type.IMPLEMENTATION, dependency.fullPath)
}
fun DependencyHandler.testImplementation(dependency: Dependency) {
add(Type.TEST_IMPLEMENTATION, dependency.fullPath)
}
fun DependencyHandler.androidTestImplementation(dependency: Dependency) {
add(Type.ANDROID_TEST_IMPLEMENTATION, dependency.fullPath)
}
fun DependencyHandler.api(dependency: Dependency) {
add(Type.API, dependency.fullPath)
}
fun DependencyHandler.kapt(dependency: Dependency) {
add(Type.KAPT, dependency.fullPath)
}
fun DependencyHandler.ksp(dependency: Dependency) {
add(Type.KSP, dependency.fullPath)
}
fun DependencyHandler.defaultLibrary() {
api(Dependencies.AndroidX.core)
api(Dependencies.AndroidX.appcompat)
api(Dependencies.AndroidX.materialDesign)
}
private object Type {
const val IMPLEMENTATION = "implementation"
const val TEST_IMPLEMENTATION = "testImplementation"
const val ANDROID_TEST_IMPLEMENTATION = "androidTestImplementation"
const val API = "api"
const val KAPT = "kapt"
const val KSP = "ksp"
}

View File

@ -0,0 +1,67 @@
import org.gradle.kotlin.dsl.version
import org.gradle.plugin.use.PluginDependenciesSpec
import org.gradle.plugin.use.PluginDependencySpec
val PluginDependenciesSpec.androidApplication: PluginDependencySpec
get() = id(Plugin.Id.Android.application)
val PluginDependenciesSpec.androidLibrary: PluginDependencySpec
get() = id(Plugin.Id.Android.library)
val PluginDependenciesSpec.kotlinJvm: PluginDependencySpec
get() = id(Plugin.Id.Kotlin.jvm)
val PluginDependenciesSpec.kotlinAndroid: PluginDependencySpec
get() = id(Plugin.Id.Kotlin.android)
val PluginDependenciesSpec.kotlinParcelize: PluginDependencySpec
get() = id(Plugin.Id.Kotlin.parcelize)
val PluginDependenciesSpec.kotlinAnnotationProcessor: PluginDependencySpec
get() = id(Plugin.Id.Kotlin.annotationProcessor)
val PluginDependenciesSpec.kotlinSerialization: PluginDependencySpec
get() = id(Plugin.Id.Kotlin.serialization)
val PluginDependenciesSpec.jetbrainsKotlinSerialization: PluginDependencySpec
get() = id(Plugin.Id.JetBrains.serialization)
object Plugin {
object Id {
object Android {
/**
* [Documentation](https://google.github.io/android-gradle-dsl/current/)
* [Changelog](https://developer.android.com/studio/releases/gradle-plugin)
*/
const val application = "com.android.application"
/**
* [Documentation](https://google.github.io/android-gradle-dsl/current/)
* [Changelog](https://developer.android.com/studio/releases/gradle-plugin)
*/
const val library = "com.android.library"
}
object Kotlin {
/**
* Plugin published in https://plugins.gradle.org/
*/
const val jvm = "org.jetbrains.kotlin.jvm"
/**
* Plugin published in https://plugins.gradle.org/
*/
const val android = "org.jetbrains.kotlin.android"
/**
* Plugin published in https://plugins.gradle.org/
*/
const val parcelize = "kotlin-parcelize"
/**
* Plugin published in https://plugins.gradle.org/
*/
const val annotationProcessor = "org.jetbrains.kotlin.kapt"
/**
* Plugin published in https://plugins.gradle.org/
*/
const val serialization = "plugin.serialization"
}
object JetBrains {
const val serialization = "org.jetbrains.kotlin.plugin.serialization"
}
}
}

View File

@ -0,0 +1,42 @@
import org.gradle.api.JavaVersion
object Version {
/**
* Gradle is an open-source build automation tool focused on flexibility and performance.
*
* [Documentation](https://docs.gradle.org/current/userguide/userguide.html)
*
* [Github](https://github.com/gradle/gradle)
*
* [Apache 2.0 License](https://github.com/gradle/gradle/blob/master/LICENSE)
*
* [Changelog](https://gradle.org/releases/)
*/
const val agp = "8.7.1"
object Kotlin {
/**
* [Documentation](https://kotlinlang.org/)
*
* [Source Code](https://github.com/JetBrains/kotlin/)
*
* [Apache 2.0 License](https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt)
*
* [Changelog](https://kotlinlang.org/releases.html)
*/
const val language = "2.0.21"
val javaSource = JavaVersion.VERSION_11
const val jvmTarget = "11"
}
object Android {
object Sdk {
const val min = 24
const val compile = 34
const val target = 34
}
}
}

21
gradle.properties Normal file
View File

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

11
gradle/README.md Normal file
View File

@ -0,0 +1,11 @@
# Файл для управления дистрибутивом Gradle
Данный репозиторий необходим для поддержки актуальности дистрибутива Gradle в проектах. Данный проект необходимо подключать подмодулём и использовать во всех проектах.
## Как обвновлять дистрибутив
Перед обновлением необходимо удостоверится, что версия самого Gradle установлена не ниже, чем в дистрибутиве. Сделать это можно [здесь](https://sicampus.ru/gitea/core/dependecies/src/branch/main/src/main/java/Version.kt#L16).
Процесс обновления выглядит следуюющим образом:
1. В начале обновляем саму версию Gradle ([в этом репозитории](https://sicampus.ru/gitea/core/dependecies)).
2. Обновлем ссылку на дестрибутив и проверяем совместимость.

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Thu Jan 04 22:32:26 NOVT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

17
settings.gradle.kts Normal file
View File

@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Work"
include(":app")