Initial commit

This commit is contained in:
yastruckov 2025-02-18 17:47:55 +03:00
commit 67dfbaa59c
102 changed files with 3272 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

View File

@ -0,0 +1,82 @@
kotlin version: 2.0.21
error message: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:195)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:128)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:170)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:136)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:165)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:134)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.nio.file.DirectoryNotEmptyException: C:\Users\yastr\AppData\Local\Temp\kotlin-backups17205006845707631203
at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(Unknown Source)
at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(Unknown Source)
at java.base/java.nio.file.Files.delete(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash$lambda$11$lambda$10$lambda$9(CompilationTransaction.kt:244)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.ArrayList.forEach(Unknown Source)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(Unknown Source)
at java.base/java.util.stream.Sink$ChainedReference.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.close(CompilationTransaction.kt:254)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:747)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
... 3 more

View File

@ -0,0 +1,82 @@
kotlin version: 2.0.21
error message: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:195)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:128)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:170)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:136)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:165)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:134)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.nio.file.DirectoryNotEmptyException: C:\Users\yastr\AppData\Local\Temp\kotlin-backups11302413676619982638
at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(Unknown Source)
at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(Unknown Source)
at java.base/java.nio.file.Files.delete(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash$lambda$11$lambda$10$lambda$9(CompilationTransaction.kt:244)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.ArrayList.forEach(Unknown Source)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(Unknown Source)
at java.base/java.util.stream.Sink$ChainedReference.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.close(CompilationTransaction.kt:254)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:747)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
... 3 more

View File

@ -0,0 +1,82 @@
kotlin version: 2.0.21
error message: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:195)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:128)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:170)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:136)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:165)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:134)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.nio.file.DirectoryNotEmptyException: C:\Users\yastr\AppData\Local\Temp\kotlin-backups4231236667734128612
at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(Unknown Source)
at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(Unknown Source)
at java.base/java.nio.file.Files.delete(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash$lambda$11$lambda$10$lambda$9(CompilationTransaction.kt:244)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.ArrayList.forEach(Unknown Source)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(Unknown Source)
at java.base/java.util.stream.Sink$ChainedReference.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.close(CompilationTransaction.kt:254)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:747)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
... 3 more

View File

@ -0,0 +1,82 @@
kotlin version: 2.0.21
error message: Daemon compilation failed: null
java.lang.Exception
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:195)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:128)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:170)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:136)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:165)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:134)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.nio.file.DirectoryNotEmptyException: C:\Users\yastr\AppData\Local\Temp\kotlin-backups2526552628484561697
at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(Unknown Source)
at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(Unknown Source)
at java.base/java.nio.file.Files.delete(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction$cleanupStash$2$1$1.invoke(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash$lambda$11$lambda$10$lambda$9(CompilationTransaction.kt:244)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.ArrayList.forEach(Unknown Source)
at java.base/java.util.stream.SortedOps$RefSortingSink.end(Unknown Source)
at java.base/java.util.stream.Sink$ChainedReference.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.cleanupStash(CompilationTransaction.kt:244)
at org.jetbrains.kotlin.incremental.RecoverableCompilationTransaction.close(CompilationTransaction.kt:254)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:747)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:120)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
... 3 more

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

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

@ -0,0 +1,76 @@
plugins {
androidApplication
jetbrainsKotlinSerialization version Version.Kotlin.language
kotlinAnnotationProcessor
id("com.google.dagger.hilt.android") version("2.51.1")
alias(libs.plugins.kotlin.android)
}
val packageName = "ru.myitschool.work"
android {
namespace = packageName
compileSdk = 35
defaultConfig {
applicationId = packageName
minSdk = 31
targetSdk = 35
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 {
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.annotation)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
defaultLibrary()
implementation(Dependencies.AndroidX.activity)
implementation(Dependencies.AndroidX.fragment)
implementation(Dependencies.AndroidX.constraintLayout)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.serialization)
implementation(libs.picasso)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.datastore.preferences)
implementation(libs.barcode.scanning)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.mlkit.vision)
val hilt = "2.51.1"
implementation(libs.hilt.android)
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,38 @@
<?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:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Default"
tools:targetApi="35">
<activity
android:name=".ui.RootActivity"
android:exported="true"
android:theme="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</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,5 @@
package ru.myitschool.work.core
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
object Constants {
const val SERVER_ADDRESS = "http://10.0.2.2:8080"
}

View File

@ -0,0 +1,33 @@
package ru.myitschool.work.data
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "user_login")
class UserDataStoreManager(private val context: Context) {
companion object {
private val USERNAME_KEY = stringPreferencesKey("username")
fun getInstance(context: Context): UserDataStoreManager {
return UserDataStoreManager(context.applicationContext)
}
}
val usernameFlow: Flow<String> = context.applicationContext.dataStore.data.map { prefs ->
prefs[USERNAME_KEY] ?: ""
}
suspend fun saveUsername(username: String) {
context.dataStore.edit { prefs ->
prefs[USERNAME_KEY] = username
}
}
suspend fun clearUsername() {
context.applicationContext.dataStore.edit { it.clear() }
}
}

View File

@ -0,0 +1,39 @@
package ru.myitschool.work.data.door
import android.content.Context
import io.ktor.client.call.body
import io.ktor.client.request.patch
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.dto.OpenRequestDTO
import ru.myitschool.work.utils.NetworkModule
class DoorNetworkDataSource(
context: Context
) {
private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun openDoor(openRequestDTO: OpenRequestDTO): Result<Unit> = withContext(Dispatchers.IO){
runCatching {
val username = userDataStoreManager.usernameFlow.first()
val result = client.patch("${Constants.SERVER_ADDRESS}/api/$username/open"){
contentType(ContentType.Application.Json)
setBody(openRequestDTO)
}
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
println(result.bodyAsText())
result.body()
}
}
}

View File

@ -0,0 +1,12 @@
package ru.myitschool.work.data.door
import ru.myitschool.work.domain.door.DoorRepo
import ru.myitschool.work.domain.entities.OpenEntity
class DoorRepoImpl(
private val networkDataSource: DoorNetworkDataSource
) : DoorRepo {
override suspend fun openDoor(openEntity: OpenEntity): Result<Unit> {
return networkDataSource.openDoor(openEntity.toDto())
}
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class OpenRequestDTO(
@SerialName("value") val value: Long
)

View File

@ -0,0 +1,15 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserDTO(
@SerialName("id") val id: Int,
@SerialName("login") val login: String,
@SerialName("name") val name: String,
@SerialName("photo") val photo: String,
@SerialName("position") val position: String,
@SerialName("lastVisit") val lastVisit: String
)

View File

@ -0,0 +1,34 @@
package ru.myitschool.work.data.info
import android.content.Context
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.dto.UserDTO
import ru.myitschool.work.utils.NetworkModule
class InfoNetworkDataSource(
context: Context
) {
private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun getInfo():Result<UserDTO> = withContext(Dispatchers.IO){
runCatching {
val username = userDataStoreManager.usernameFlow.first()
val result = client.get("${Constants.SERVER_ADDRESS}/api/$username/info")
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
println(result.bodyAsText())
result.body()
}
}
}

View File

@ -0,0 +1,19 @@
package ru.myitschool.work.data.info
import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.domain.info.InfoRepo
class InfoRepoImpl(
private val networkDataSource: InfoNetworkDataSource
): InfoRepo {
override suspend fun getInfo(): Result<UserEntity> {
return networkDataSource.getInfo().map { dto ->
UserEntity(
name = dto.name,
position = dto.position,
lastVisit = dto.lastVisit,
photo = dto.photo
)
}
}
}

View File

@ -0,0 +1,26 @@
package ru.myitschool.work.data.login
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.utils.NetworkModule
class LoginNetworkDataSource {
private val client = NetworkModule.httpClient
suspend fun login(username: String):Result<Unit> = withContext(Dispatchers.IO){
runCatching {
val result = client.get("${Constants.SERVER_ADDRESS}/api/$username/auth")
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
println(result.bodyAsText())
result.body()
}
}
}

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.data.login
import ru.myitschool.work.domain.login.LoginRepo
class LoginRepoImpl(
private val networkDataSource: LoginNetworkDataSource
) : LoginRepo {
override suspend fun login(username: String): Result<Unit> {
return networkDataSource.login(username)
}
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.door
import ru.myitschool.work.domain.entities.OpenEntity
interface DoorRepo {
suspend fun openDoor(openEntity: OpenEntity) : Result<Unit>
}

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.domain.door
import ru.myitschool.work.domain.entities.OpenEntity
class OpenDoorUseCase(
private val repo: DoorRepo
) {
suspend operator fun invoke(openEntity: OpenEntity) = repo.openDoor(
openEntity = openEntity
)
}

View File

@ -0,0 +1,13 @@
package ru.myitschool.work.domain.entities
import ru.myitschool.work.data.dto.OpenRequestDTO
data class OpenEntity(
val value: Long
){
fun toDto() : OpenRequestDTO{
return OpenRequestDTO(
value = value
)
}
}

View File

@ -0,0 +1,8 @@
package ru.myitschool.work.domain.entities
data class UserEntity (
val name: String,
val photo: String,
val position: String,
val lastVisit: String
)

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.info
class GetInfoUseCase(
private val repo: InfoRepo
) {
suspend operator fun invoke() = repo.getInfo()
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.info
import ru.myitschool.work.domain.entities.UserEntity
interface InfoRepo {
suspend fun getInfo(): Result<UserEntity>
}

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.domain.login
interface LoginRepo {
suspend fun login(username: String): Result<Unit>
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.login
class LoginUseCase(
private val repo: LoginRepo
) {
suspend operator fun invoke(username : String) = repo.login(username)
}

View File

@ -0,0 +1,27 @@
package ru.myitschool.work.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R
import ru.myitschool.work.databinding.ActivityRootBinding
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint
class RootActivity : AppCompatActivity() {
private lateinit var binding: ActivityRootBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRootBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val windowInsetsController = window.insetsController
windowInsetsController?.hide(android.view.WindowInsets.Type.statusBars() or android.view.WindowInsets.Type.navigationBars())
}
}

View File

@ -0,0 +1,59 @@
package ru.myitschool.work.ui.login
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentLoginBinding
class LoginFragment : Fragment(R.layout.fragment_login) {
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
private val viewModel: LoginViewModel by viewModels{ LoginViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentLoginBinding.bind(view)
val textWatcher = object : TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun afterTextChanged(s: Editable?) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val username = binding.username.text
binding.loginBtn.isEnabled = username.length >= 3 && !username[0].isDigit() && username.matches(Regex("^[a-zA-Z0-9]*$"))
}
}
binding.username.addTextChangedListener(textWatcher)
binding.loginBtn.isEnabled = false
binding.loginBtn.setOnClickListener{
viewModel.login(binding.username.text.toString())
}
lifecycleScope.launch {
viewModel.state.collect { state ->
with(binding) {
error.visibility = if (state is LoginViewModel.State.Error) View.VISIBLE else View.GONE
username.isEnabled = state !is LoginViewModel.State.Loading
if (state is LoginViewModel.State.Success) {
findNavController().navigate(R.id.mainFragment)
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,76 @@
package ru.myitschool.work.ui.login
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.login.LoginNetworkDataSource
import ru.myitschool.work.data.login.LoginRepoImpl
import ru.myitschool.work.domain.login.LoginUseCase
class LoginViewModel(
private val useCase: LoginUseCase,
application: Application
) : AndroidViewModel(application) {
private val dataStoreManager = UserDataStoreManager(application)
private val _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state.asStateFlow()
init {
viewModelScope.launch{
val username = dataStoreManager.usernameFlow.first()
if(username != "")
login(username)
}
}
sealed class State {
object Idle : State()
object Loading : State()
object Success : State()
data class Error(val message: String?) : State()
}
fun login(username: String) {
_state.value = State.Loading
viewModelScope.launch{
useCase.invoke(username).fold(
onSuccess = { data ->
dataStoreManager.saveUsername(username)
_state.value = State.Success
},
onFailure = {e->
println(e)
_state.value = State.Error(e.message)
}
)
}
}
companion object {
@Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val repoImpl = LoginRepoImpl(
networkDataSource = LoginNetworkDataSource()
)
val useCase = LoginUseCase(repoImpl)
return LoginViewModel(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T
}
}
}
}

View File

@ -0,0 +1,117 @@
package ru.myitschool.work.ui.main
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.squareup.picasso.Picasso
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentMainBinding
import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.utils.UserState
import ru.myitschool.work.utils.collectWhenStarted
class MainFragment : Fragment(R.layout.fragment_main) {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private val viewModel: MainViewModel by viewModels{ MainViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentMainBinding.bind(view)
viewModel.getUserData()
binding.refresh.setOnClickListener { viewModel.getUserData() }
binding.logout.setOnClickListener { logout() }
binding.scan.setOnClickListener { onScanClick() }
viewModel.userState.collectWhenStarted(this) { state ->
when (state) {
is UserState.Error -> {
showError()
binding.loading.visibility = View.GONE
binding.refresh.visibility = View.VISIBLE
}
is UserState.Loading -> {
binding.loading.visibility = View.VISIBLE
setViewsVisibility(View.GONE)
binding.refresh.visibility = View.GONE
}
is UserState.Success -> {
setViewsVisibility(View.VISIBLE)
binding.refresh.visibility = View.VISIBLE
binding.loading.visibility = View.GONE
binding.error.visibility = View.GONE
showUserData(state.userEntity)
}
}
}
setFragmentResultListener(QrScanDestination.REQUEST_KEY) { _, bundle ->
val qrData = QrScanDestination.getDataIfExist(bundle)
println(qrData)
val bundleToQrResult = bundleOf("qr_data" to qrData)
findNavController().navigate(R.id.qrResultFragment, bundleToQrResult)
}
}
private fun logout() {
lifecycleScope.launch {
viewModel.clearUsername()
delay(50) // Не всегда успевает скинуть логин
findNavController().navigate(R.id.loginFragment)
}
}
private fun showUserData(userEntity: UserEntity) {
binding.apply {
fullname.text = userEntity.name
position.text = userEntity.position
lastEntry.text = viewModel.formatDate(userEntity.lastVisit)
Picasso.get().load(userEntity.photo).into(photo)
error.visibility = View.GONE
setViewsVisibility(View.VISIBLE)
}
}
private fun showError() {
binding.error.visibility = View.VISIBLE
setViewsVisibility(View.GONE)
}
private fun setViewsVisibility(visibility: Int) {
binding.fullname.visibility = visibility
binding.position.visibility = visibility
binding.lastEntry.visibility = visibility
binding.photo.visibility = visibility
binding.logout.visibility = visibility
binding.scan.visibility = visibility
}
private fun onScanClick() {
findNavController().navigate(R.id.qrScanFragment)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,81 @@
package ru.myitschool.work.ui.main
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.info.InfoNetworkDataSource
import ru.myitschool.work.data.info.InfoRepoImpl
import ru.myitschool.work.domain.info.GetInfoUseCase
import ru.myitschool.work.utils.UserState
import java.text.SimpleDateFormat
import java.util.Locale
class MainViewModel(
private val useCase: GetInfoUseCase,
application: Application
) : AndroidViewModel(application) {
private val _userState = MutableStateFlow<UserState>(UserState.Loading)
val userState: StateFlow<UserState> get() = _userState
private val dataStoreManager = UserDataStoreManager(application)
fun formatDate(date: String): String {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
val outputFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return try {
val formattedDate = inputFormat.parse(date)
if (formattedDate != null) {
outputFormat.format(formattedDate)
} else{}
} catch (_: Exception) {
"Invalid Date"
}.toString()
}
fun getUserData() {
_userState.value = UserState.Loading
viewModelScope.launch {
useCase.invoke().fold(
onSuccess = { data -> _userState.value = UserState.Success(data) },
onFailure = { _userState.value = UserState.Error }
)
}
}
fun clearUsername() {
viewModelScope.launch{
withContext(Dispatchers.IO) {
dataStoreManager.clearUsername()
}
}
}
companion object {
@Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val repoImpl = InfoRepoImpl(
networkDataSource = InfoNetworkDataSource(
context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
)
)
val useCase = GetInfoUseCase(repoImpl)
return MainViewModel(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T
}
}
}
}

View File

@ -0,0 +1,46 @@
package ru.myitschool.work.ui.qr.result
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentQrResultBinding
import ru.myitschool.work.domain.entities.OpenEntity
import ru.myitschool.work.utils.collectWhenStarted
class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
private var _binding: FragmentQrResultBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<QrResultViewModel>{ QrResultViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentQrResultBinding.bind(view)
var qrData = arguments?.getString("qr_data")
if (qrData != null) {
viewModel.openDoor(OpenEntity(qrData.toLong()))
}
else{
binding.result.text = getString(R.string.result_null_text)
}
viewModel.state.collectWhenStarted(this){ state->
when(state) {
QrResultViewModel.State.Error -> {
binding.result.text = getString(R.string.result_fail_text)
}
QrResultViewModel.State.Loading -> {
Unit
}
QrResultViewModel.State.Success -> {
binding.result.text = getString(R.string.result_success_text)
}
}
binding.close.setOnClickListener{
qrData = ""
findNavController().navigate(R.id.mainFragment)
}
}
}
}

View File

@ -0,0 +1,62 @@
package ru.myitschool.work.ui.qr.result
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.door.DoorNetworkDataSource
import ru.myitschool.work.data.door.DoorRepoImpl
import ru.myitschool.work.domain.door.OpenDoorUseCase
import ru.myitschool.work.domain.entities.OpenEntity
class QrResultViewModel(
private val useCase: OpenDoorUseCase,
application: Application
) : AndroidViewModel(application) {
private val _state = MutableStateFlow<State>(State.Loading)
val state: StateFlow<State> = _state.asStateFlow()
sealed class State{
object Success : State()
object Loading : State()
object Error : State()
}
fun openDoor(openEntity: OpenEntity){
_state.value = State.Loading
viewModelScope.launch{
useCase.invoke(openEntity).fold(
onSuccess = { data->
_state.value = State.Success
},
onFailure = { _ ->
_state.value = State.Error
}
)
}
}
companion object {
@Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val repoImpl = DoorRepoImpl(
networkDataSource = DoorNetworkDataSource(
context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
)
)
val useCase = OpenDoorUseCase(repoImpl)
return QrResultViewModel(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T
}
}
}
}

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,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,20 @@
package ru.myitschool.work.utils
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
object NetworkModule {
val httpClient: HttpClient by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
isLenient = true
ignoreUnknownKeys = true
})
}
}
}
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.utils
import ru.myitschool.work.domain.entities.UserEntity
sealed class UserState {
object Loading : UserState()
data class Success(val userEntity: UserEntity) : UserState()
object Error : UserState()
}

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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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>

Binary file not shown.

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Montserrat&amp;weight=700"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Montserrat&amp;weight=500"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

View File

@ -0,0 +1,20 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.RootActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,83 @@
<androidx.constraintlayout.widget.ConstraintLayout 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/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="ru.myitschool.work.ui.login.LoginFragment">
<LinearLayout
android:layout_width="342dp"
android:layout_height="220dp"
android:layout_marginStart="32dp"
android:layout_marginTop="250dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="350dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/montserrat_bold"
android:text="@string/login_title"
android:textAlignment="center"
android:textSize="20sp" />
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="61dp"
android:layout_marginTop="20sp"
android:autofillHints=""
android:ems="10"
android:fontFamily="@font/montserrat_medium"
android:hint="@string/username_hint"
android:inputType="text"
android:textAlignment="textStart"
android:textColorLink="#000000" />
<Button
android:id="@+id/loginBtn"
android:layout_width="312dp"
android:layout_height="68dp"
android:layout_marginStart="10sp"
android:layout_marginEnd="10sp"
android:backgroundTint="@color/enable_btn"
android:enabled="false"
android:fontFamily="@font/montserrat_bold"
android:text="@string/login_btn"
android:textColor="@color/white"
android:textSize="16sp"
app:cornerRadius="10sp" />
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="43dp"
android:fontFamily="@font/montserrat_bold"
android:text="@string/login_error"
android:textAlignment="center"
android:textColor="@color/red"
android:textSize="20sp"
android:visibility="gone"
tools:visibility="gone" />
</LinearLayout>
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,128 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.238">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
android:visibility="visible"
tools:visibility="visible">
<TextView
android:id="@+id/fullname"
android:layout_width="match_parent"
android:layout_height="57dp"
android:layout_marginBottom="30sp"
android:fontFamily="@font/montserrat_bold"
android:textAlignment="center"
android:textSize="24sp"
tools:text="@tools:sample/full_names" />
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="188dp"
android:contentDescription="@string/app_name"
app:srcCompat="@drawable/avatar"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20sp"
android:fontFamily="@font/montserrat_medium"
android:textAlignment="center"
android:textSize="20sp"
tools:text="Разработчик" />
<TextView
android:id="@+id/lastEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10sp"
android:fontFamily="@font/montserrat_medium"
android:textAlignment="center"
android:textSize="20sp"
tools:text="@tools:sample/date/mmddyy" />
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="43dp"
android:fontFamily="@font/montserrat_bold"
android:text="@string/main_error"
android:textAlignment="center"
android:textColor="@color/red"
android:textSize="20sp"
android:visibility="gone" />
</LinearLayout>
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="gone" />
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="64dp"
android:orientation="vertical">
<Button
android:id="@+id/scan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/teal_700"
android:fontFamily="@font/montserrat_bold"
android:text="@string/scan_btn"
android:textColor="@color/white" />
<Button
android:id="@+id/logout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/teal_700"
android:fontFamily="@font/montserrat_bold"
android:text="@string/logout_btn"
android:textColor="@color/white" />
</LinearLayout>
<Button
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginEnd="64dp"
android:backgroundTint="@color/teal_700"
android:fontFamily="@font/montserrat_bold"
android:text="@string/refresh_btn"
android:textColor="@color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="50dp"
android:layout_marginTop="200dp"
android:layout_marginEnd="50dp"
android:layout_marginBottom="200dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/montserrat_bold"
android:text="@string/result_fail_text"
android:textAlignment="center"
android:textSize="20sp" />
<Button
android:id="@+id/close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="75sp"
android:layout_marginTop="20sp"
android:layout_marginEnd="75sp"
android:backgroundTint="#009688"
android:fontFamily="@font/montserrat_bold"
android:text="@string/close_btn" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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,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,43 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph"
app:startDestination="@id/loginFragment">
<fragment
android:id="@+id/loginFragment"
android:name="ru.myitschool.work.ui.login.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login" >
<action
android:id="@+id/action_loginFragment_to_mainFragment"
app:destination="@id/mainFragment" />
</fragment>
<fragment
android:id="@+id/mainFragment"
android:name="ru.myitschool.work.ui.main.MainFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_main">
<action
android:id="@+id/action_mainFragment_to_qrScanFragment"
app:destination="@id/qrScanFragment" />
</fragment>
<fragment
android:id="@+id/qrScanFragment"
android:name="ru.myitschool.work.ui.qr.scan.QrScanFragment"
android:label="QrScanFragment"
tools:layout="@layout/fragment_qr_scan">
<action
android:id="@+id/action_qrScanFragment_to_qrResultFragment"
app:destination="@id/qrResultFragment" />
</fragment>
<fragment
android:id="@+id/qrResultFragment"
android:name="ru.myitschool.work.ui.qr.result.QrResultFragment"
android:label="QrResultFragment"
tools:layout="@layout/fragment_qr_result">
<action
android:id="@+id/action_qrResultFragment_to_mainFragment"
app:destination="@id/mainFragment" />
</fragment>
</navigation>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NTO Pass</string>
<string name="login_title">Login</string>
<string name="username_hint">Enter your username</string>
<string name="login_btn">Login</string>
<string name="login_error">Login Error</string>
<string name="main_title">Main page</string>
<string name="logout_btn">Logout</string>
<string name="refresh_btn">Refresh</string>
<string name="scan_btn">Scan</string>
<string name="main_error">Error</string>
<string name="result_success_text">Success</string>
<string name="result_fail_text">Something wrong</string>
<string name="close_btn">Close</string>
<string name="close_button">Close</string>
<string name="result_null_text">Operation was cancelled</string>
</resources>

View File

@ -0,0 +1,14 @@
<?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>
<color name="red">#E4080A</color>
<color name="enable_btn">#E78A07</color>
<color name="disable_btn">#C3C3C3</color>
<color name="gray">#E78A07</color>
</resources>

View File

@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="fragment_horizontal_margin">16dp</dimen>
<dimen name="fragment_vertical_margin">16dp</dimen>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev">
<item>
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod">
<item>
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</item>
</string-array>
</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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="preloaded_fonts" translatable="false">
<item>@font/montserrat_bold</item>
<item>@font/montserrat_medium</item>
</array>
</resources>

View File

@ -0,0 +1,21 @@
<resources>
<string name="app_name">NTO Pass</string>
<string name="login_title">Логин</string>
<string name="username_hint">Введите ваше имя пользователя</string>
<string name="login_btn">Логин</string>
<string name="login_error">Ошибка логина</string>
<string name="main_title">Главная страница</string>
<string name="logout_btn">Выйти из аккаунта</string>
<string name="refresh_btn">Перезагрузить</string>
<string name="scan_btn">Сканировать</string>
<string name="main_error">Ошибка</string>
<string name="result_success_text">Успешно</string>
<string name="result_fail_text">Что-то пошло не так</string>
<string name="result_null_text">Вход был отменён</string>
<string name="close_btn">Закрыть</string>
<!-- TODO: Remove or change this placeholder text -->
</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>
<!-- 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,5 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true"
tools:ignore="InsecureBaseConfiguration" />
</network-security-config>

8
build.gradle.kts Normal file
View File

@ -0,0 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
androidApplication version "8.8.0" 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
alias(libs.plugins.kotlin.android) 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.8.0"
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 = 31
const val compile = 35
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. Обновлем ссылку на дестрибутив и проверяем совместимость.

47
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,47 @@
[versions]
activityKtx = "1.10.0"
barcodeScanning = "17.3.0"
cameraX = "1.4.1"
datastorePreferences = "1.1.2"
hilt = "2.51.1"
kotlin = "2.1.0"
appcompat = "1.7.0"
kotlinxSerializationJson = "1.7.3"
ktorClientContentNegotiation = "3.0.3"
ktorClientSerialization = "2.3.7"
ktorSerializationKotlinxJson = "3.0.3"
material = "1.12.0"
annotation = "1.9.1"
constraintlayout = "2.2.0"
lifecycleLivedataKtx = "2.8.7"
lifecycleViewmodelKtx = "2.8.7"
navigationFragmentKtx = "2.8.7"
navigationUiKtx = "2.8.7"
picasso = "2.8"
[libraries]
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraX" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraX" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraX" }
androidx-camera-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "cameraX" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraX" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationUiKtx" }
androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientContentNegotiation" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientContentNegotiation" }
ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktorClientSerialization" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorSerializationKotlinxJson" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientContentNegotiation" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

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

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Sat Feb 15 16:14:42 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-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" "$@"

Some files were not shown because too many files have changed in this diff Show More