commit 1ac20aec4f4f208bdfcb0427fb4d36a681a40799 Author: varksyu Date: Tue Feb 18 18:21:25 2025 +0300 first commit: add template diff --git a/.gitea/workflows/automerge-with-core.yaml b/.gitea/workflows/automerge-with-core.yaml new file mode 100644 index 0000000..e040dbd --- /dev/null +++ b/.gitea/workflows/automerge-with-core.yaml @@ -0,0 +1,26 @@ +name: Merge core/template-android-project to this repo + +env: + CORE_REPO: "https://git.sicampus.ru/core/template-android-project.git" + TOKEN: ${{ secrets.PUSH_TOKEN }} + +run-name: Merge core/template-android-project to ${{ gitea.repository }} +on: + schedule: + - cron: '@daily' + + +jobs: + merge-if-needed: + if: ${{ !contains(gitea.repository, 'core/template-android-project' ) }} + runs-on: ubuntu-latest + steps: + - run: echo "Merge core/template-android-project to ${{ gitea.repository }}" + - name: Check out repository code + uses: actions/checkout@v4 + - name: Sync repos + uses: Vova-SH/sync-upstream-repo@1.0.5 + with: + upstream_repo: ${{ env.CORE_REPO }} + token: ${{ env.TOKEN }} + spawn_logs: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b3f5536 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "gradle"] + path = gradle + url = https://git.sicampus.ru/core/gradle.git +[submodule "buildSrc"] + path = buildSrc + url = https://git.sicampus.ru/core/dependecies.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef91da2 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +[![Android Studio version](https://img.shields.io/endpoint?url=https%3A%2F%2Fsicampus.ru%2Fgitea%2Fcore%2Fdocs%2Fraw%2Fbranch%2Fmain%2Fandroid-studio-label.json)](https://sicampus.ru/gitea/core/docs/src/branch/main/how-upload-project.md) + +# НТО 2024. II отборочный этап. Командные задани — клиентская часть + +## 📖 Предыстория +В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных. +Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него. + + + +## 📋 Системные требования + +| **Параметр** | **Требование** | +|-----------------------------|---------------------------------------| +| **Минимальная версия Android** | 9.0 (API 28) | +| **Целевая версия Android** | 14 (API 34) | +| **Поддерживаемые устройства** | смартфоны, планшеты | +| **Ориентация экранов** | портретная | +| **Языки** | русский, английский | +| **Разрешения** | доступ к интернету, камера (при необходимости) | + + + +## 📱 Техническое задание +Требуется разработать нативное мобильное приложение, которое будет содержать следующие экраны. + + +### 1. Экран авторизации + +> Данный экран должен быть показан при первом входе в приложение, а также в ситуациях, когда пользователь не зарегистрировался в приложении. + +#### Элементы, которые должны присутствовать на экране: +- Поле ввода (`id/username`), в котором пользователю необходимо ввести свой логин. +- Кнопка входа (`id/login`), по нажатию на которую пользователь авторизуется в системе. +- По умолчанию скрытое (`GONE`) текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +1. В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю. +2. Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной: + - Поле ввода пустое. + - Количество символов менее 3х. + - Логин начинается с цифры. + - Логин содержит символы, отличные от латинского алфавита и цифр. +3. Поле ввода и кнопку должно быть видно при раскрытии клавиатуры. +4. - При нажатии на кнопку входа необходимо проверить, что данный пользователь существует с помощью запроса `api//auth` (подробное описание представлено в техническом задании серверной части). +5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку. +6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан. +7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно. +8. Экран авторизации показывается только в случае, если пользователь неавторизован. + + + + +### 2. Главный экран + +> Данный экран содержит общую информацию о пользователе: +>- ФИО +>- Фото +>- Должность +>- Время последнего входа + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/fullname`), в котором написано имя пользователя. +- Изображение (`id/photo`), на котором отображено фото пользователя. +- Текстовое поле (`id/position`), в котором написана должность пользователя. +- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя. +- Кнопка (`id/logout`) для выхода пользователя из аккаунта. +- Кнопка (`id/refresh`) для обновления данных. +- Кнопка (`id/scan`) для сканирования QR кода. +- По умолчанию скрытое текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. +- Для получения данных необходимо использовать сетевой запрос `/api//info`. +- Формат даты и времени последнего входа пользователя: `yyyy-MM-dd HH:mm` (например: 2024-02-31 08:31). Время необходимо отображать с сервера, без поправок на часовой пояс или локальное смещение. +- При нажатии на кнопку выход все данные (если есть) пользователя должны быть очищены, а приложение должно открыть экран авторизации. +- При нажатии кнопки сканирования необходимо открыть экран сканирования QR кода. +- При нажатии на кнопку обновления данных - необходимо повторно вызывать сетевой запрос для получения актуальных данных. + + + +### 3. Экран сканирования QR-кода + +> Данный экран позволяет отсканировать код на турникете и войти с помощью смартфона. В данном случае данный экран будет уже написан и представлен dам в готовом виде в заготовке. Вам необходимо только подписаться на его результат с помощью **Result API** и обработать считанные данные из QR кода. **Данный экран нельзя модифицировать. Он поставляется как есть.** + + + +### 4. Экран с результатом сканирования QR кода + +> На данном экране необходимо вывести успешность или неуспешность входа с помощью метода QR кода. + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/result`), где содержится текст об успешности или неуспешности входа. +- Кнопка (`id/close`) для закрытия данного экрана. + +#### Требования к компонентам: +- В случае, если результат пришёл пустым или со статусом “Отмена” - необходимо вывести пользователю текст: + *"Вход был отменён/Operation was cancelled"* +- В случае, если данные пришли, то необходимо их отправить на сервер с запросом `api//open`, добавив данные из результата и получить ответ. +- Если сервер ответил успешно - то отображаем текст: + *"Успешно/Success"* +- Если сервер ответил любой ошибкой - то отображаем текст: + *"Что-то пошло не так/Something wrong"* +- Кнопка закрытия всегда открывает главный экран. + + + +## 🛠 Решение + +Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149). + +Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах). + + + +## ✅ Особенности оценивания + +Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения. +В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. +Сервер и приложение тестируются независимо. + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a28d464 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + kotlinAndroid + androidApplication + jetbrainsKotlinSerialization version Version.Kotlin.language + kotlinAnnotationProcessor + id("com.google.dagger.hilt.android").version("2.51.1") +} + +val packageName = "ru.myitschool.work" + +android { + namespace = packageName + compileSdk = Version.Android.Sdk.compile + + defaultConfig { + applicationId = packageName + minSdk = Version.Android.Sdk.min + targetSdk = Version.Android.Sdk.target + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures.viewBinding = true + + compileOptions { + sourceCompatibility = Version.Kotlin.javaSource + targetCompatibility = Version.Kotlin.javaSource + } + + kotlinOptions { + jvmTarget = Version.Kotlin.jvmTarget + } +} + +dependencies { + defaultLibrary() + + implementation(Dependencies.AndroidX.activity) + implementation(Dependencies.AndroidX.fragment) + implementation(Dependencies.AndroidX.constraintLayout) + + implementation(Dependencies.AndroidX.Navigation.fragment) + implementation(Dependencies.AndroidX.Navigation.navigationUi) + + implementation(Dependencies.Retrofit.library) + implementation(Dependencies.Retrofit.gsonConverter) + + implementation("com.squareup.picasso:picasso:2.8") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("com.google.mlkit:barcode-scanning:17.3.0") + + val cameraX = "1.3.4" + implementation("androidx.camera:camera-core:$cameraX") + implementation("androidx.camera:camera-camera2:$cameraX") + implementation("androidx.camera:camera-lifecycle:$cameraX") + implementation("androidx.camera:camera-view:$cameraX") + implementation("androidx.camera:camera-mlkit-vision:1.4.0-rc04") + + val hilt = "2.51.1" + implementation("com.google.dagger:hilt-android:$hilt") + kapt("com.google.dagger:hilt-android-compiler:$hilt") +} + +kapt { + correctErrorTypes = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a986978 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt new file mode 100644 index 0000000..3085135 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt new file mode 100644 index 0000000..971f65a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.core +// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ +object Constants { + const val SERVER_ADDRESS = "http://localhost:8090" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt new file mode 100644 index 0000000..88a796a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -0,0 +1,56 @@ +package ru.myitschool.work.ui + +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.createGraph +import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.fragment +import dagger.hilt.android.AndroidEntryPoint +import ru.myitschool.work.R +import ru.myitschool.work.ui.login.LoginDestination +import ru.myitschool.work.ui.login.LoginFragment +import ru.myitschool.work.ui.qr.scan.QrScanDestination +import ru.myitschool.work.ui.qr.scan.QrScanFragment + +// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! +@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 = LoginDestination + ) { + fragment() + fragment() + } + } + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onSupportNavigateUp() + } + } + ) + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment) + val popBackResult = if (navController.previousBackStackEntry != null) { + navController.popBackStack() + } else { + false + } + return popBackResult || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt new file mode 100644 index 0000000..50acfb0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.login + +import kotlinx.serialization.Serializable + +@Serializable +data object LoginDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt new file mode 100644 index 0000000..02842ce --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -0,0 +1,36 @@ +package ru.myitschool.work.ui.login + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentLoginBinding +import ru.myitschool.work.utils.collectWhenStarted +import ru.myitschool.work.utils.visibleOrGone + +@AndroidEntryPoint +class LoginFragment : Fragment(R.layout.fragment_login) { + private var _binding: FragmentLoginBinding? = null + private val binding: FragmentLoginBinding get() = _binding!! + + private val viewModel: LoginViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentLoginBinding.bind(view) + subscribe() + } + + private fun subscribe() { + viewModel.state.collectWhenStarted(this) { state -> + binding.loading.visibleOrGone(state) + } + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..3a53d6c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt new file mode 100644 index 0000000..7e34b28 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt new file mode 100644 index 0000000..a9ddaab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt new file mode 100644 index 0000000..14565ab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt @@ -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() + val action = _action.asSharedFlow() + + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + init { + checkPermission() + } + + fun onPermissionResult(isGranted: Boolean) { + viewModelScope.launch { + if (isGranted) { + _state.update { State.Scan } + } else { + _action.emit(Action.CloseWithCancel) + } + } + } + + private fun checkPermission() { + viewModelScope.launch { + val isPermissionGranted = ContextCompat.checkSelfPermission( + getApplication(), + CAMERA_PERMISSION + ) == PackageManager.PERMISSION_GRANTED + if (isPermissionGranted) { + _state.update { State.Scan } + } else { + delay(1000) + _action.emit(Action.RequestPermission(CAMERA_PERMISSION)) + } + } + } + + fun findBarcode(barcode: Barcode) { + viewModelScope.launch { + barcode.rawValue?.let { value -> + _action.emit(Action.CloseWithResult(value)) + } + } + } + + fun close() { + viewModelScope.launch { + _action.emit(Action.CloseWithCancel) + } + } + + sealed interface State { + data object Loading : State + + data object Scan : State + } + + sealed interface Action { + data class RequestPermission( + val permission: String + ) : Action + data object CloseWithCancel : Action + data class CloseWithResult( + val result: String + ) : Action + } + + private companion object { + val initialState = State.Loading + + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt new file mode 100644 index 0000000..87bccc2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.utils + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow + +fun MutablePublishFlow() = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + BufferOverflow.DROP_OLDEST +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt new file mode 100644 index 0000000..8c99ef3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt @@ -0,0 +1,18 @@ +package ru.myitschool.work.utils + +import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +inline fun Flow.collectWhenStarted( + fragment: Fragment, + crossinline collector: (T) -> Unit +) { + fragment.viewLifecycleOwner.lifecycleScope.launch { + flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> + collector.invoke(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt new file mode 100644 index 0000000..c81147d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.utils + +import android.text.Editable +import android.text.TextWatcher + +open class TextChangedListener: TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) = Unit +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt new file mode 100644 index 0000000..5c38f67 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.utils + +import android.view.View + +fun View.visibleOrGone(isVisible: Boolean) { + this.visibility = if (isVisible) View.VISIBLE else View.GONE +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..c22a96f --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_no_img.xml b/app/src/main/res/drawable/ic_no_img.xml new file mode 100644 index 0000000..44206c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_img.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 0000000..b03f9ae --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..86504d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml new file mode 100644 index 0000000..e7cb1a9 --- /dev/null +++ b/app/src/main/res/layout/activity_root.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..7f3cd66 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_qr_scan.xml b/app/src/main/res/layout/fragment_qr_scan.xml new file mode 100644 index 0000000..a52eb71 --- /dev/null +++ b/app/src/main/res/layout/fragment_qr_scan.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..ce65075 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b183019 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + NTO Pass + \ No newline at end of file diff --git a/app/src/main/res/values/strings_qr.xml b/app/src/main/res/values/strings_qr.xml new file mode 100644 index 0000000..ce50067 --- /dev/null +++ b/app/src/main/res/values/strings_qr.xml @@ -0,0 +1,4 @@ + + + Close + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..89e63d4 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..176d19a --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4a92e0e --- /dev/null +++ b/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 0000000..6fbe8a4 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,2 @@ +/.gradle +/build \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..4866817 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt new file mode 100644 index 0000000..59a2e2d --- /dev/null +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/DependencyHandlerExtensions.kt b/buildSrc/src/main/java/DependencyHandlerExtensions.kt new file mode 100644 index 0000000..12065c1 --- /dev/null +++ b/buildSrc/src/main/java/DependencyHandlerExtensions.kt @@ -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" +} diff --git a/buildSrc/src/main/java/Plugin.kt b/buildSrc/src/main/java/Plugin.kt new file mode 100644 index 0000000..e575b9b --- /dev/null +++ b/buildSrc/src/main/java/Plugin.kt @@ -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" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Version.kt b/buildSrc/src/main/java/Version.kt new file mode 100644 index 0000000..7b7f7e8 --- /dev/null +++ b/buildSrc/src/main/java/Version.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3e927b1 --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/README.md b/gradle/README.md new file mode 100644 index 0000000..de403e9 --- /dev/null +++ b/gradle/README.md @@ -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. Обновлем ссылку на дестрибутив и проверяем совместимость. \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bfd6fc2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..4da3ae4 --- /dev/null +++ b/settings.gradle.kts @@ -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")