commit 67dfbaa59ca94196f39cfbaf2a34c1b5a8823ae6 Author: yastruckov Date: Tue Feb 18 17:47:55 2025 +0300 Initial commit diff --git a/.gitea/workflows/automerge-with-core.yaml b/.gitea/workflows/automerge-with-core.yaml new file mode 100644 index 0000000..e040dbd --- /dev/null +++ b/.gitea/workflows/automerge-with-core.yaml @@ -0,0 +1,26 @@ +name: Merge core/template-android-project to this repo + +env: + CORE_REPO: "https://git.sicampus.ru/core/template-android-project.git" + TOKEN: ${{ secrets.PUSH_TOKEN }} + +run-name: Merge core/template-android-project to ${{ gitea.repository }} +on: + schedule: + - cron: '@daily' + + +jobs: + merge-if-needed: + if: ${{ !contains(gitea.repository, 'core/template-android-project' ) }} + runs-on: ubuntu-latest + steps: + - run: echo "Merge core/template-android-project to ${{ gitea.repository }}" + - name: Check out repository code + uses: actions/checkout@v4 + - name: Sync repos + uses: Vova-SH/sync-upstream-repo@1.0.5 + with: + upstream_repo: ${{ env.CORE_REPO }} + token: ${{ env.TOKEN }} + spawn_logs: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b3f5536 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "gradle"] + path = gradle + url = https://git.sicampus.ru/core/gradle.git +[submodule "buildSrc"] + path = buildSrc + url = https://git.sicampus.ru/core/dependecies.git diff --git a/.kotlin/errors/errors-1732305455885.log b/.kotlin/errors/errors-1732305455885.log new file mode 100644 index 0000000..9922b32 --- /dev/null +++ b/.kotlin/errors/errors-1732305455885.log @@ -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 + + diff --git a/.kotlin/errors/errors-1732467098950.log b/.kotlin/errors/errors-1732467098950.log new file mode 100644 index 0000000..102944b --- /dev/null +++ b/.kotlin/errors/errors-1732467098950.log @@ -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 + + diff --git a/.kotlin/errors/errors-1732726114629.log b/.kotlin/errors/errors-1732726114629.log new file mode 100644 index 0000000..f473f72 --- /dev/null +++ b/.kotlin/errors/errors-1732726114629.log @@ -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 + + diff --git a/.kotlin/errors/errors-1732729015076.log b/.kotlin/errors/errors-1732729015076.log new file mode 100644 index 0000000..3b20c8e --- /dev/null +++ b/.kotlin/errors/errors-1732729015076.log @@ -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 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef91da2 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +[![Android Studio version](https://img.shields.io/endpoint?url=https%3A%2F%2Fsicampus.ru%2Fgitea%2Fcore%2Fdocs%2Fraw%2Fbranch%2Fmain%2Fandroid-studio-label.json)](https://sicampus.ru/gitea/core/docs/src/branch/main/how-upload-project.md) + +# НТО 2024. II отборочный этап. Командные задани — клиентская часть + +## 📖 Предыстория +В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных. +Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него. + + + +## 📋 Системные требования + +| **Параметр** | **Требование** | +|-----------------------------|---------------------------------------| +| **Минимальная версия Android** | 9.0 (API 28) | +| **Целевая версия Android** | 14 (API 34) | +| **Поддерживаемые устройства** | смартфоны, планшеты | +| **Ориентация экранов** | портретная | +| **Языки** | русский, английский | +| **Разрешения** | доступ к интернету, камера (при необходимости) | + + + +## 📱 Техническое задание +Требуется разработать нативное мобильное приложение, которое будет содержать следующие экраны. + + +### 1. Экран авторизации + +> Данный экран должен быть показан при первом входе в приложение, а также в ситуациях, когда пользователь не зарегистрировался в приложении. + +#### Элементы, которые должны присутствовать на экране: +- Поле ввода (`id/username`), в котором пользователю необходимо ввести свой логин. +- Кнопка входа (`id/login`), по нажатию на которую пользователь авторизуется в системе. +- По умолчанию скрытое (`GONE`) текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +1. В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю. +2. Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной: + - Поле ввода пустое. + - Количество символов менее 3х. + - Логин начинается с цифры. + - Логин содержит символы, отличные от латинского алфавита и цифр. +3. Поле ввода и кнопку должно быть видно при раскрытии клавиатуры. +4. - При нажатии на кнопку входа необходимо проверить, что данный пользователь существует с помощью запроса `api//auth` (подробное описание представлено в техническом задании серверной части). +5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку. +6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан. +7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно. +8. Экран авторизации показывается только в случае, если пользователь неавторизован. + + + + +### 2. Главный экран + +> Данный экран содержит общую информацию о пользователе: +>- ФИО +>- Фото +>- Должность +>- Время последнего входа + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/fullname`), в котором написано имя пользователя. +- Изображение (`id/photo`), на котором отображено фото пользователя. +- Текстовое поле (`id/position`), в котором написана должность пользователя. +- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя. +- Кнопка (`id/logout`) для выхода пользователя из аккаунта. +- Кнопка (`id/refresh`) для обновления данных. +- Кнопка (`id/scan`) для сканирования QR кода. +- По умолчанию скрытое текстовое поле с ошибкой (`id/error`). + +#### Требования к компонентам: +- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. +- Для получения данных необходимо использовать сетевой запрос `/api//info`. +- Формат даты и времени последнего входа пользователя: `yyyy-MM-dd HH:mm` (например: 2024-02-31 08:31). Время необходимо отображать с сервера, без поправок на часовой пояс или локальное смещение. +- При нажатии на кнопку выход все данные (если есть) пользователя должны быть очищены, а приложение должно открыть экран авторизации. +- При нажатии кнопки сканирования необходимо открыть экран сканирования QR кода. +- При нажатии на кнопку обновления данных - необходимо повторно вызывать сетевой запрос для получения актуальных данных. + + + +### 3. Экран сканирования QR-кода + +> Данный экран позволяет отсканировать код на турникете и войти с помощью смартфона. В данном случае данный экран будет уже написан и представлен dам в готовом виде в заготовке. Вам необходимо только подписаться на его результат с помощью **Result API** и обработать считанные данные из QR кода. **Данный экран нельзя модифицировать. Он поставляется как есть.** + + + +### 4. Экран с результатом сканирования QR кода + +> На данном экране необходимо вывести успешность или неуспешность входа с помощью метода QR кода. + +#### Элементы, которые должны присутствовать на экране: +- Текстовое поле (`id/result`), где содержится текст об успешности или неуспешности входа. +- Кнопка (`id/close`) для закрытия данного экрана. + +#### Требования к компонентам: +- В случае, если результат пришёл пустым или со статусом “Отмена” - необходимо вывести пользователю текст: + *"Вход был отменён/Operation was cancelled"* +- В случае, если данные пришли, то необходимо их отправить на сервер с запросом `api//open`, добавив данные из результата и получить ответ. +- Если сервер ответил успешно - то отображаем текст: + *"Успешно/Success"* +- Если сервер ответил любой ошибкой - то отображаем текст: + *"Что-то пошло не так/Something wrong"* +- Кнопка закрытия всегда открывает главный экран. + + + +## 🛠 Решение + +Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149). + +Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах). + + + +## ✅ Особенности оценивания + +Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения. +В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. +Сервер и приложение тестируются независимо. + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..4bdcaaa --- /dev/null +++ b/app/build.gradle.kts @@ -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 +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3033ffc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt new file mode 100644 index 0000000..3085135 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt new file mode 100644 index 0000000..ffb63c1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.core +// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ +object Constants { + const val SERVER_ADDRESS = "http://10.0.2.2:8080" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/UserDataStoreManager.kt b/app/src/main/java/ru/myitschool/work/data/UserDataStoreManager.kt new file mode 100644 index 0000000..f24174a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/UserDataStoreManager.kt @@ -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 = 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() } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/door/DoorNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/door/DoorNetworkDataSource.kt new file mode 100644 index 0000000..4589597 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/door/DoorNetworkDataSource.kt @@ -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 = 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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/door/DoorRepoImpl.kt b/app/src/main/java/ru/myitschool/work/data/door/DoorRepoImpl.kt new file mode 100644 index 0000000..510c241 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/door/DoorRepoImpl.kt @@ -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 { + return networkDataSource.openDoor(openEntity.toDto()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/OpenRequestDTO.kt b/app/src/main/java/ru/myitschool/work/data/dto/OpenRequestDTO.kt new file mode 100644 index 0000000..159c5a0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/OpenRequestDTO.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/UserDTO.kt b/app/src/main/java/ru/myitschool/work/data/dto/UserDTO.kt new file mode 100644 index 0000000..3e0e4be --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/UserDTO.kt @@ -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 +) + diff --git a/app/src/main/java/ru/myitschool/work/data/info/InfoNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/info/InfoNetworkDataSource.kt new file mode 100644 index 0000000..1b38515 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/info/InfoNetworkDataSource.kt @@ -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 = 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/info/InfoRepoImpl.kt b/app/src/main/java/ru/myitschool/work/data/info/InfoRepoImpl.kt new file mode 100644 index 0000000..9c82918 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/info/InfoRepoImpl.kt @@ -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 { + return networkDataSource.getInfo().map { dto -> + UserEntity( + name = dto.name, + position = dto.position, + lastVisit = dto.lastVisit, + photo = dto.photo + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/login/LoginNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/login/LoginNetworkDataSource.kt new file mode 100644 index 0000000..a2b5063 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/login/LoginNetworkDataSource.kt @@ -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 = 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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/login/LoginRepoImpl.kt b/app/src/main/java/ru/myitschool/work/data/login/LoginRepoImpl.kt new file mode 100644 index 0000000..890931b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/login/LoginRepoImpl.kt @@ -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 { + return networkDataSource.login(username) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/door/DoorRepo.kt b/app/src/main/java/ru/myitschool/work/domain/door/DoorRepo.kt new file mode 100644 index 0000000..bd96676 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/door/DoorRepo.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/door/OpenDoorUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/door/OpenDoorUseCase.kt new file mode 100644 index 0000000..f968d4a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/door/OpenDoorUseCase.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/OpenEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/OpenEntity.kt new file mode 100644 index 0000000..288b068 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/OpenEntity.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt new file mode 100644 index 0000000..4500cb7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/info/GetInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/info/GetInfoUseCase.kt new file mode 100644 index 0000000..72760f7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/info/GetInfoUseCase.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.domain.info + +class GetInfoUseCase( + private val repo: InfoRepo +) { + suspend operator fun invoke() = repo.getInfo() +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/info/InfoRepo.kt b/app/src/main/java/ru/myitschool/work/domain/info/InfoRepo.kt new file mode 100644 index 0000000..39b68c6 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/info/InfoRepo.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.domain.info + +import ru.myitschool.work.domain.entities.UserEntity + +interface InfoRepo { + suspend fun getInfo(): Result +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/login/LoginRepo.kt b/app/src/main/java/ru/myitschool/work/domain/login/LoginRepo.kt new file mode 100644 index 0000000..c63ac08 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/login/LoginRepo.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.domain.login + +interface LoginRepo { + suspend fun login(username: String): Result +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/login/LoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/login/LoginUseCase.kt new file mode 100644 index 0000000..5798888 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/login/LoginUseCase.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt new file mode 100644 index 0000000..edb7a46 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -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()) + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt new file mode 100644 index 0000000..4056cc9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..e6f1502 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt @@ -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.Idle) + val state: StateFlow = _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 create(modelClass: Class, extras: CreationExtras): T { + val repoImpl = LoginRepoImpl( + networkDataSource = LoginNetworkDataSource() + ) + + val useCase = LoginUseCase(repoImpl) + + return LoginViewModel( + useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application + ) as T + } + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainFragment.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainFragment.kt new file mode 100644 index 0000000..69c4b06 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainFragment.kt @@ -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 + } + } + diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt new file mode 100644 index 0000000..641f149 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt @@ -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.Loading) + val userState: StateFlow 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 create(modelClass: Class, 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 + } + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt new file mode 100644 index 0000000..2ac5b52 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt @@ -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.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) + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt new file mode 100644 index 0000000..d9620e2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt @@ -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.Loading) + val state: StateFlow = _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 create(modelClass: Class, 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 + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt new file mode 100644 index 0000000..7e34b28 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt @@ -0,0 +1,30 @@ +package ru.myitschool.work.ui.qr.scan + +import android.os.Bundle +import androidx.core.os.bundleOf +import kotlinx.serialization.Serializable + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +@Serializable +data object QrScanDestination { + const val REQUEST_KEY = "qr_result" + private const val KEY_QR_DATA = "key_qr" + + fun newInstance(): QrScanFragment { + return QrScanFragment() + } + + fun getDataIfExist(bundle: Bundle): String? { + return if (bundle.containsKey(KEY_QR_DATA)) { + bundle.getString(KEY_QR_DATA) + } else { + null + } + } + + internal fun packToBundle(data: String): Bundle { + return bundleOf( + KEY_QR_DATA to data + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt new file mode 100644 index 0000000..a9ddaab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt @@ -0,0 +1,139 @@ +package ru.myitschool.work.ui.qr.scan + +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.ImageAnalysis +import androidx.camera.mlkit.vision.MlKitAnalyzer +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentQrScanBinding +import ru.myitschool.work.utils.collectWhenStarted +import ru.myitschool.work.utils.visibleOrGone + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +class QrScanFragment : Fragment(R.layout.fragment_qr_scan) { + private var _binding: FragmentQrScanBinding? = null + private val binding: FragmentQrScanBinding get() = _binding!! + + private var barcodeScanner: BarcodeScanner? = null + private var isCameraInit: Boolean = false + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> viewModel.onPermissionResult(isGranted) } + + private val viewModel: QrScanViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentQrScanBinding.bind(view) + sendResult(bundleOf()) + subscribe() + initCallback() + } + + private fun initCallback() { + binding.close.setOnClickListener { viewModel.close() } + } + + private fun subscribe() { + viewModel.state.collectWhenStarted(this) { state -> + binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading) + binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan) + if (!isCameraInit && state is QrScanViewModel.State.Scan) { + startCamera() + isCameraInit = true + } + } + + viewModel.action.collectWhenStarted(this) { action -> + when (action) { + is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission) + is QrScanViewModel.Action.CloseWithCancel -> { + goBack() + } + is QrScanViewModel.Action.CloseWithResult -> { + sendResult(QrScanDestination.packToBundle(action.result)) + goBack() + } + } + } + } + + private fun requestPermission(permission: String) { + permissionLauncher.launch(permission) + } + + private fun startCamera() { + val context = requireContext() + val cameraController = LifecycleCameraController(context) + val previewView: PreviewView = binding.viewFinder + val executor = ContextCompat.getMainExecutor(context) + + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + val barcodeScanner = BarcodeScanning.getClient(options) + this.barcodeScanner = barcodeScanner + + cameraController.setImageAnalysisAnalyzer( + executor, + MlKitAnalyzer( + listOf(barcodeScanner), + ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, + executor + ) { result -> + result?.getValue(barcodeScanner)?.firstOrNull()?.let { value -> + viewModel.findBarcode(value) + + } + } + ) + + cameraController.bindToLifecycle(this) + previewView.controller = cameraController + } + + override fun onDestroyView() { + barcodeScanner?.close() + barcodeScanner = null + _binding = null + super.onDestroyView() + } + + private fun goBack() { + findNavControllerOrNull()?.popBackStack() + ?: requireActivity().onBackPressedDispatcher.onBackPressed() + } + + private fun sendResult(bundle: Bundle) { + setFragmentResult( + QrScanDestination.REQUEST_KEY, + bundle + ) + findNavControllerOrNull() + ?.previousBackStackEntry + ?.savedStateHandle + ?.set(QrScanDestination.REQUEST_KEY, bundle) + } + + private fun findNavControllerOrNull(): NavController? { + return try { + findNavController() + } catch (_: Throwable) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt new file mode 100644 index 0000000..14565ab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt @@ -0,0 +1,93 @@ +package ru.myitschool.work.ui.qr.scan + +import android.Manifest +import android.app.Application +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.google.mlkit.vision.barcode.common.Barcode +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.myitschool.work.utils.MutablePublishFlow + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +class QrScanViewModel( + application: Application +) : AndroidViewModel(application) { + + private val _action = MutablePublishFlow() + val action = _action.asSharedFlow() + + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + init { + checkPermission() + } + + fun onPermissionResult(isGranted: Boolean) { + viewModelScope.launch { + if (isGranted) { + _state.update { State.Scan } + } else { + _action.emit(Action.CloseWithCancel) + } + } + } + + private fun checkPermission() { + viewModelScope.launch { + val isPermissionGranted = ContextCompat.checkSelfPermission( + getApplication(), + CAMERA_PERMISSION + ) == PackageManager.PERMISSION_GRANTED + if (isPermissionGranted) { + _state.update { State.Scan } + } else { + delay(1000) + _action.emit(Action.RequestPermission(CAMERA_PERMISSION)) + } + } + } + + fun findBarcode(barcode: Barcode) { + viewModelScope.launch { + barcode.rawValue?.let { value -> + _action.emit(Action.CloseWithResult(value)) + } + } + } + + fun close() { + viewModelScope.launch { + _action.emit(Action.CloseWithCancel) + } + } + + sealed interface State { + data object Loading : State + + data object Scan : State + } + + sealed interface Action { + data class RequestPermission( + val permission: String + ) : Action + data object CloseWithCancel : Action + data class CloseWithResult( + val result: String + ) : Action + } + + private companion object { + val initialState = State.Loading + + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt new file mode 100644 index 0000000..87bccc2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.utils + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow + +fun MutablePublishFlow() = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + BufferOverflow.DROP_OLDEST +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt new file mode 100644 index 0000000..8c99ef3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt @@ -0,0 +1,18 @@ +package ru.myitschool.work.utils + +import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +inline fun Flow.collectWhenStarted( + fragment: Fragment, + crossinline collector: (T) -> Unit +) { + fragment.viewLifecycleOwner.lifecycleScope.launch { + flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> + collector.invoke(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/NetworkModule.kt b/app/src/main/java/ru/myitschool/work/utils/NetworkModule.kt new file mode 100644 index 0000000..fd3afb2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/NetworkModule.kt @@ -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 + }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/UserState.kt b/app/src/main/java/ru/myitschool/work/utils/UserState.kt new file mode 100644 index 0000000..81d83c3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/UserState.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt new file mode 100644 index 0000000..5c38f67 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.utils + +import android.view.View + +fun View.visibleOrGone(isVisible: Boolean) { + this.visibility = if (isVisible) View.VISIBLE else View.GONE +} \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar.jpg b/app/src/main/res/drawable/avatar.jpg new file mode 100644 index 0000000..bd434fe Binary files /dev/null and b/app/src/main/res/drawable/avatar.jpg differ diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..c22a96f --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_no_img.xml b/app/src/main/res/drawable/ic_no_img.xml new file mode 100644 index 0000000..44206c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_img.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 0000000..b03f9ae --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..86504d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/font/montserrat_bold.ttf b/app/src/main/res/font/montserrat_bold.ttf new file mode 100644 index 0000000..4df50da Binary files /dev/null and b/app/src/main/res/font/montserrat_bold.ttf differ diff --git a/app/src/main/res/font/montserrat_bold.xml b/app/src/main/res/font/montserrat_bold.xml new file mode 100644 index 0000000..afbba1b --- /dev/null +++ b/app/src/main/res/font/montserrat_bold.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/montserrat_medium.xml b/app/src/main/res/font/montserrat_medium.xml new file mode 100644 index 0000000..131ca24 --- /dev/null +++ b/app/src/main/res/font/montserrat_medium.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml new file mode 100644 index 0000000..ecc6a87 --- /dev/null +++ b/app/src/main/res/layout/activity_root.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..59512e8 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,83 @@ + + + + + + + + +