Compare commits
213 Commits
main
...
generic-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c1f77bc8 | ||
|
|
f00774af08 | ||
|
|
e7c52b9f43 | ||
|
|
b507137b82 | ||
|
|
b395df6268 | ||
|
|
27058baf99 | ||
|
|
419d5b6cf1 | ||
|
|
e0d13f3bce | ||
|
|
2ad327cde8 | ||
|
|
196a127110 | ||
|
|
951bd2f8c6 | ||
|
|
9925b9b8aa | ||
|
|
48b336aade | ||
|
|
0b8320ce29 | ||
|
|
767bc0ea75 | ||
|
|
00359d3b8d | ||
|
|
4f40559417 | ||
|
|
2fefeee7ac | ||
|
|
02b660d1ca | ||
|
|
5f969ac651 | ||
|
|
057f71e292 | ||
|
|
74300239a3 | ||
|
|
5ade8d4d83 | ||
|
|
d501d806e7 | ||
|
|
abbbd28095 | ||
|
|
8f9c912f41 | ||
|
|
70aafe73be | ||
|
|
5a5da9fe72 | ||
|
|
c9fd5d7993 | ||
|
|
30adc892d4 | ||
|
|
ed001bd266 | ||
|
|
60cb946518 | ||
|
|
053579b685 | ||
|
|
0736a481c2 | ||
|
|
ec9ae99e03 | ||
|
|
3ebfd949eb | ||
|
|
af34397f88 | ||
|
|
b1a0b7b753 | ||
|
|
95a2cd7e93 | ||
|
|
945f9920fd | ||
|
|
7adc89b4f3 | ||
|
|
1883ef2316 | ||
|
|
97c3ac172d | ||
|
|
9f69174cc7 | ||
|
|
d7cb04a782 | ||
|
|
273fbd50be | ||
|
|
140a01c208 | ||
|
|
10eac95eec | ||
|
|
183904838d | ||
|
|
fa75748e2c | ||
|
|
cccbecf934 | ||
|
|
b5012f12a9 | ||
|
|
972ca81b03 | ||
|
|
53a339c8a6 | ||
|
|
0eb3f8290d | ||
|
|
e41adc5720 | ||
|
|
b49997889d | ||
|
|
6e2092a2b0 | ||
|
|
89577cf867 | ||
|
|
506ac3a9b9 | ||
|
|
c1f60baffb | ||
|
|
f41af9f130 | ||
|
|
fe823fea1d | ||
|
|
77e9f5d2ac | ||
|
|
78b408e41a | ||
|
|
b18c9e33c2 | ||
|
|
3a976d85d1 | ||
|
|
72a1cea7a5 | ||
|
|
beabe015ca | ||
|
|
5b507bbefd | ||
|
|
4fc60e4180 | ||
|
|
edb933146b | ||
|
|
9684559e70 | ||
|
|
3de809e23f | ||
|
|
9275095d66 | ||
|
|
744d92221e | ||
|
|
161ac01820 | ||
|
|
d4641e8f4d | ||
|
|
e32ff01664 | ||
|
|
fc47c86049 | ||
|
|
e6e4586ee9 | ||
|
|
32124886c5 | ||
|
|
fdf160b7a1 | ||
|
|
cf6b44b083 | ||
|
|
f7d540c6eb | ||
|
|
ff3a90aa9a | ||
|
|
196d9f8eb5 | ||
|
|
65b95eda2f | ||
|
|
7a22437bd5 | ||
|
|
1482c2b548 | ||
|
|
bdda880414 | ||
|
|
e137b0695f | ||
|
|
52882ac8a8 | ||
|
|
bc7cb275db | ||
|
|
ac10e6ac01 | ||
|
|
58581101a5 | ||
|
|
6054ba4ac1 | ||
|
|
bbfd204b7d | ||
|
|
0920d72d2e | ||
|
|
c45af45481 | ||
|
|
8731f27f83 | ||
|
|
ac9728174a | ||
|
|
f1c9af72b7 | ||
|
|
bcd8c77887 | ||
|
|
71dfac73f6 | ||
|
|
cb628a684b | ||
|
|
66832733d9 | ||
|
|
c47f75e9f2 | ||
|
|
3d144f3876 | ||
|
|
5afbcc6670 | ||
|
|
58dd00db11 | ||
|
|
810bbfc327 | ||
|
|
1efbb9af0f | ||
|
|
204a0084da | ||
|
|
fcc831a63d | ||
|
|
ac3a4ebe0f | ||
|
|
ee1f6b8884 | ||
|
|
e435e0ff14 | ||
|
|
de07673a29 | ||
|
|
de4a5c9f8d | ||
|
|
6c322efd93 | ||
|
|
72d1fcdaea | ||
|
|
7d51ac210e | ||
|
|
fc803c6fe2 | ||
|
|
7cc572f201 | ||
|
|
bb59f43dfe | ||
|
|
a48f7ff007 | ||
|
|
c5b52711d0 | ||
|
|
2f70040f54 | ||
|
|
fcbda41419 | ||
|
|
13556dd84c | ||
|
|
7d63df0c83 | ||
|
|
814f9c5c48 | ||
|
|
e8cc833455 | ||
|
|
cdc3a9cf75 | ||
|
|
6b9f54184e | ||
|
|
10b06b2019 | ||
|
|
d1b31bc645 | ||
|
|
0447cc5b60 | ||
|
|
f578e537e0 | ||
|
|
3f0f10e2c6 | ||
|
|
527de78877 | ||
|
|
18bcfb7549 | ||
|
|
18cd04ed92 | ||
|
|
5d8fa4cb54 | ||
|
|
96048ee619 | ||
|
|
05495f2e60 | ||
|
|
d41f15925f | ||
|
|
54c309d4f6 | ||
|
|
fb3029e138 | ||
|
|
a5a314f39f | ||
|
|
8187c0b2dc | ||
|
|
d87fa7de80 | ||
|
|
41fca7375e | ||
|
|
6777faefef | ||
|
|
7c23d1b493 | ||
|
|
dfea713a26 | ||
|
|
4c2e12f075 | ||
|
|
39f43596ae | ||
|
|
b23eb10f04 | ||
|
|
17eb17a9d9 | ||
|
|
f27cddc464 | ||
|
|
4d3440fbac | ||
|
|
81597efd5c | ||
|
|
7bcc899eb9 | ||
|
|
d0ca0f88b8 | ||
|
|
a4482ee342 | ||
|
|
498bb1a38e | ||
|
|
a46ba87d44 | ||
|
|
b24e22b6d7 | ||
|
|
0a44e7eb92 | ||
|
|
61fc5132b2 | ||
|
|
032f007be6 | ||
|
|
d926526440 | ||
|
|
737de0785c | ||
|
|
8add42f1de | ||
|
|
f8e67f1a7e | ||
|
|
234b595e1f | ||
|
|
a9e548db83 | ||
|
|
e82aa38917 | ||
|
|
ef2ecefea9 | ||
|
|
14aefcf3ae | ||
|
|
9f4c4c7057 | ||
|
|
bbbbb460ad | ||
|
|
0595e791a1 | ||
|
|
81e4351695 | ||
|
|
394074e0ce | ||
|
|
3971b58472 | ||
|
|
7b3e4465bd | ||
|
|
c2de7bdd11 | ||
|
|
42ac1fc8c4 | ||
|
|
dcee750197 | ||
|
|
1ecaeb5fc1 | ||
|
|
ed354c7799 | ||
|
|
6ccab6e184 | ||
|
|
5246c8be89 | ||
|
|
43f75b454f | ||
|
|
1fcab8ae14 | ||
|
|
fa54dcba1c | ||
|
|
6018fd73d3 | ||
|
|
a7aff3c680 | ||
|
|
2372dd9589 | ||
|
|
adf1a80ae8 | ||
|
|
679fa0ef69 | ||
|
|
b6d1133da2 | ||
|
|
ad3d6f974e | ||
|
|
29edb0f115 | ||
|
|
d16a6b9c44 | ||
|
|
251f91128a | ||
|
|
cf64155a41 | ||
|
|
8524a1b56e | ||
|
|
9cafedc9a0 | ||
|
|
0a889bdbea |
101
.gitlab-ci.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# To contribute improvements to CI/CD templates, please follow the Development guide at:
|
||||
# https://docs.gitlab.com/ee/development/cicd/templates.html
|
||||
# This specific template is located at:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Android.gitlab-ci.yml
|
||||
|
||||
# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
|
||||
# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.
|
||||
|
||||
image: eclipse-temurin:17-jdk-jammy
|
||||
|
||||
variables:
|
||||
|
||||
# ANDROID_COMPILE_SDK is the version of Android you're compiling with.
|
||||
# It should match compileSdkVersion.
|
||||
ANDROID_COMPILE_SDK: "34"
|
||||
|
||||
# ANDROID_BUILD_TOOLS is the version of the Android build tools you are using.
|
||||
# It should match buildToolsVersion.
|
||||
ANDROID_BUILD_TOOLS: "33.0.2"
|
||||
|
||||
# It's what version of the command line tools we're going to download from the official site.
|
||||
# Official Site-> https://developer.android.com/studio/index.html
|
||||
# There, look down below at the cli tools only, sdk tools package is of format:
|
||||
# commandlinetools-os_type-ANDROID_SDK_TOOLS_latest.zip
|
||||
# when the script was last modified for latest compileSdkVersion, it was which is written down below
|
||||
ANDROID_SDK_TOOLS: "9477386"
|
||||
|
||||
# Packages installation before running script
|
||||
before_script:
|
||||
- apt-get --quiet update --yes
|
||||
- apt-get --quiet install --yes wget unzip
|
||||
|
||||
# Setup path as android_home for moving/exporting the downloaded sdk into it
|
||||
- export ANDROID_HOME="${PWD}/android-sdk-root"
|
||||
# Create a new directory at specified location
|
||||
- install -d $ANDROID_HOME
|
||||
# Here we are installing androidSDK tools from official source,
|
||||
# (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well)
|
||||
# after that unzipping those tools and
|
||||
# then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build
|
||||
- wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
|
||||
- unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip"
|
||||
- mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
|
||||
- export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin
|
||||
|
||||
# Nothing fancy here, just checking sdkManager version
|
||||
- sdkmanager --version
|
||||
|
||||
# use yes to accept all licenses
|
||||
- yes | sdkmanager --licenses > /dev/null || true
|
||||
- sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}"
|
||||
- sdkmanager "platform-tools"
|
||||
- sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}"
|
||||
|
||||
# Not necessary, but just for surity
|
||||
- chmod +x ./gradlew
|
||||
|
||||
# Basic android and gradle stuff
|
||||
# Check linting
|
||||
lintDebug:
|
||||
interruptible: true
|
||||
stage: build
|
||||
script:
|
||||
- ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
|
||||
artifacts:
|
||||
paths:
|
||||
- app/lint/reports/lint-results-debug.html
|
||||
expose_as: "lint-report"
|
||||
when: always
|
||||
|
||||
# Make Project
|
||||
assembleDebug:
|
||||
interruptible: true
|
||||
stage: build
|
||||
script:
|
||||
- ./gradlew assembleDebug
|
||||
artifacts:
|
||||
paths:
|
||||
- app/build/outputs/
|
||||
|
||||
# Run all tests, if any fails, interrupt the pipeline(fail it)
|
||||
debugTests:
|
||||
needs: [lintDebug, assembleDebug]
|
||||
interruptible: true
|
||||
stage: test
|
||||
script:
|
||||
- ./gradlew -Pci --console=plain :app:testDebug
|
||||
artifacts:
|
||||
paths:
|
||||
- app/build/test-results/
|
||||
|
||||
publishTestResults:
|
||||
stage: test
|
||||
script:
|
||||
- echo "Publishing JUnit test results"
|
||||
needs: [debugTests]
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit: app/build/test-results/testDebugUnitTest/*.xml
|
||||
11
.idea/deploymentTargetSelector.xml
generated
@@ -4,6 +4,17 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-02-06T09:50:04.018268400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R9ZW605ZHAY" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="AnimatedPreloader">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
549
.idea/other.xml
generated
Normal file
@@ -0,0 +1,549 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP5552L1" />
|
||||
<option name="id" value="OP5552L1" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2415" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OPPO" />
|
||||
<option name="codename" value="OP573DL1" />
|
||||
<option name="id" value="OP573DL1" />
|
||||
<option name="manufacturer" value="OPPO" />
|
||||
<option name="name" value="CPH2557" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a15" />
|
||||
<option name="id" value="a15" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A15" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a35x" />
|
||||
<option name="id" value="a35x" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A35" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="arcfox" />
|
||||
<option name="id" value="arcfox" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="razr plus 2024" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="1272" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="austin" />
|
||||
<option name="id" value="austin" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g 5G (2022)" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm2q" />
|
||||
<option name="id" value="dm2q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="S23 Plus" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e3q" />
|
||||
<option name="id" value="e3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24 Ultra" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3120" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="eos" />
|
||||
<option name="id" value="eos" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Eos" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="fogona" />
|
||||
<option name="id" value="fogona" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g play - 2024" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="g0q" />
|
||||
<option name="id" value="g0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-S906U1" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gta9pwifi" />
|
||||
<option name="id" value="gta9pwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-X210" />
|
||||
<option name="screenDensity" value="240" />
|
||||
<option name="screenX" value="1200" />
|
||||
<option name="screenY" value="1920" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts7xllite" />
|
||||
<option name="id" value="gts7xllite" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-T738U" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8wifi" />
|
||||
<option name="id" value="gts8wifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8" />
|
||||
<option name="screenDensity" value="274" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts9fe" />
|
||||
<option name="id" value="gts9fe" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="2304" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="maui" />
|
||||
<option name="id" value="maui" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g play - 2023" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="o1q" />
|
||||
<option name="id" value="o1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21" />
|
||||
<option name="screenDensity" value="421" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="r11q" />
|
||||
<option name="id" value="r11q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-S711U" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="t2q" />
|
||||
<option name="id" value="t2q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21 Plus" />
|
||||
<option name="screenDensity" value="394" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,7 @@
|
||||
plugins {
|
||||
kotlin("kapt")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("com.google.gms.google-services")
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
}
|
||||
@@ -8,11 +11,12 @@ android {
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "in.sminnovations.attendanceapp"
|
||||
applicationId = "in.sminnovations.attendanceapp.user"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = 11
|
||||
|
||||
versionName = "3.1.5"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -22,11 +26,12 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -47,6 +52,11 @@ android {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
// Allow references to generated code
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -59,6 +69,8 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.datastore.core.android)
|
||||
implementation(libs.androidx.datastore.preferences.core.jvm)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
@@ -66,4 +78,34 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation ("androidx.camera:camera-camera2:1.3.4")
|
||||
implementation ( "androidx.camera:camera-lifecycle:1.3.4")
|
||||
implementation ("androidx.camera:camera-view:1.3.4")
|
||||
implementation ("com.google.mlkit:barcode-scanning:17.3.0")
|
||||
implementation ("com.google.accompanist:accompanist-permissions:0.31.2-alpha")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
|
||||
|
||||
implementation ("androidx.navigation:navigation-compose:2.7.2")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||
|
||||
val nav_version = "2.8.2"
|
||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||
|
||||
// Import the Firebase BoM
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.4.0"))
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
// Add the dependency for the Firebase SDK for Google Analytics
|
||||
implementation("com.google.firebase:firebase-analytics")
|
||||
implementation ("com.google.firebase:firebase-firestore-ktx:25.1.0") // Use the latest version
|
||||
implementation("com.google.firebase:firebase-firestore")
|
||||
implementation ("com.squareup.okhttp3:okhttp:4.11.0")
|
||||
|
||||
|
||||
}
|
||||
48
app/google-services.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "482294935697",
|
||||
"project_id": "attendenceapp-57022",
|
||||
"storage_bucket": "attendenceapp-57022.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:482294935697:android:07559f83f8f740be6c8f30",
|
||||
"android_client_info": {
|
||||
"package_name": "in.sminnovations.attendanceapp.admin"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyA0H9rJ8rML0JeBvN8rx0V2ijSA0GEfKFo"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:482294935697:android:89783c6f1e52abd76c8f30",
|
||||
"android_client_info": {
|
||||
"package_name": "in.sminnovations.attendanceapp.user"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyA0H9rJ8rML0JeBvN8rx0V2ijSA0GEfKFo"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,9 +1,22 @@
|
||||
<?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-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".main.MyApp"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -13,16 +26,25 @@
|
||||
android:theme="@style/Theme.SMIAttendance"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".main.MainAppActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.SMIAttendance">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -1,47 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
SMIAttendanceTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
SMIAttendanceTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package `in`.sminnovations.attendanceapp.di
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import `in`.sminnovations.attendanceapp.utils.NetworkMonitor
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppContext(@ApplicationContext context: Context): Context {
|
||||
return context
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFirebase(): FirebaseFirestore {
|
||||
return FirebaseFirestore.getInstance()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
||||
return NetworkMonitor(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package `in`.sminnovations.attendanceapp.main
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MyApp : Application()
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package `in`.sminnovations.attendanceapp.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.BatteryManager
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainAppActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (shouldKeepScreenOn()) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
SMIAttendanceTheme {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldKeepScreenOn(): Boolean {
|
||||
val batteryStatus = registerReceiver(
|
||||
null,
|
||||
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
) ?: return false
|
||||
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||
val isCharging = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0
|
||||
val batteryPct = level * 100 / scale
|
||||
return batteryPct > 50 || isCharging
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package `in`.sminnovations.attendanceapp.main
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
|
||||
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
) {
|
||||
Scaffold(
|
||||
content = { padding ->
|
||||
Box(modifier = Modifier.padding(padding)) {
|
||||
HomeScreen()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.data
|
||||
|
||||
data class AttendanceUiState(
|
||||
val isScanning: Boolean = false,
|
||||
val qrCodeValue: String? = null,
|
||||
val userName: String? = null,
|
||||
val isCheckedIn: Boolean = false,
|
||||
val isCheckedOut: Boolean = false,
|
||||
val checkedOutTime: String = "",
|
||||
val errorMessage: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val isConnected : Boolean =true,
|
||||
val isAppDisabled : Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
import `in`.sminnovations.attendanceapp.screens.home.data.AttendanceUiState
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
internal fun AttendanceScreenContent(
|
||||
uiState: AttendanceUiState,
|
||||
cameraPermissionState: PermissionState,
|
||||
onScannerToggle: () -> Unit,
|
||||
onClickSettings: () -> Unit,
|
||||
onQrCodeScanned: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
IconButton(onClick = onClickSettings) {
|
||||
Icon(painter = painterResource(id = R.drawable.baseline_settings_24), contentDescription ="settings" )
|
||||
}
|
||||
Column {
|
||||
when {
|
||||
!cameraPermissionState.status.isGranted -> {
|
||||
RequestPermissionsContent(cameraPermissionState)
|
||||
}
|
||||
|
||||
uiState.isScanning -> {
|
||||
ScannerContent(
|
||||
onQrCodeScanned = onQrCodeScanned, onClose = onScannerToggle
|
||||
)
|
||||
}
|
||||
|
||||
uiState.errorMessage != null -> {
|
||||
ErrorContent(
|
||||
errorMessage = uiState.errorMessage,
|
||||
userName = uiState.userName,
|
||||
checkedOutTime = uiState.checkedOutTime
|
||||
)
|
||||
}
|
||||
|
||||
uiState.isCheckedIn -> {
|
||||
AttendanceStatusCard(
|
||||
title = "Checked In as", userName = uiState.userName
|
||||
)
|
||||
}
|
||||
|
||||
uiState.isCheckedOut -> {
|
||||
AttendanceStatusCard(
|
||||
title = "Checked Out as", userName = uiState.userName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.isLoading) {
|
||||
LoadingOverlay()
|
||||
}
|
||||
if (!uiState.isScanning && uiState.errorMessage == null && !uiState.isCheckedIn && !uiState.isCheckedOut && cameraPermissionState.status.isGranted){
|
||||
Box(modifier =Modifier.align(Alignment.Center) ){
|
||||
TopComponents()}
|
||||
}
|
||||
if (!uiState.isScanning && uiState.errorMessage == null && !uiState.isCheckedIn && !uiState.isCheckedOut && !cameraPermissionState.status.isGranted){
|
||||
TopComponents()
|
||||
}
|
||||
|
||||
ScannerToggleButton(
|
||||
isScanning = uiState.isScanning,
|
||||
onClick = onScannerToggle,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
internal fun AttendanceStatusCard(
|
||||
title: String, userName: String?
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OutlinedCard {
|
||||
Text(
|
||||
text = title,
|
||||
letterSpacing = 2.sp,
|
||||
lineHeight = 40.sp,
|
||||
fontSize = 30.sp,
|
||||
color = Color.Green,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(10.dp, 5.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = userName ?: "",
|
||||
letterSpacing = 2.sp,
|
||||
lineHeight = 40.sp,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
internal fun ErrorContent(
|
||||
errorMessage: String,
|
||||
userName: String?,
|
||||
checkedOutTime: String
|
||||
) {
|
||||
|
||||
val errorExplanations = mapOf(
|
||||
// Network and Data Access Errors
|
||||
"Failed to fetch employee data" to "There was an issue retrieving your employee information. Please check your internet connection and try again.",
|
||||
"Failed to fetch attendance data" to "Unable to retrieve attendance records. Please verify your internet connection and try again.",
|
||||
|
||||
// Employee Validation Errors
|
||||
"Invalid employee ID" to "The scanned ID isn't recognized. Double-check the ID, and if the issue continues, contact your administrator.",
|
||||
"Employee ID has expired" to "This ID is no longer active. Please contact HR to renew or reactivate your ID.",
|
||||
|
||||
// Attendance Process Errors
|
||||
"Invalid attendance record" to "An unexpected issue occurred with your attendance record. Please contact support for assistance.",
|
||||
"Already checked out for today." to "You've already completed your check-out for today. If you need to make corrections, please contact support.",
|
||||
"Failed to check in" to "There was an issue recording your check-in. Please try again or contact support if the problem persists.",
|
||||
"Failed to check out" to "There was an issue recording your check-out. Please try again or contact support if the problem persists.",
|
||||
|
||||
// Generic Error
|
||||
"An error occurred" to "An unexpected error occurred. Please try again or contact support if the issue continues.",
|
||||
|
||||
//Quick Login Log off
|
||||
"Wait 5 minutes before logging off" to "You have just Logged in You need to wait 5 minutes before logging in \n This behaviour may be notified to Admin 😊"
|
||||
)
|
||||
|
||||
|
||||
val detailedExplanation = errorExplanations[errorMessage] ?: "An unexpected error occurred."
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Only show the "Error Message for" text if we have a valid userName
|
||||
if (!userName.isNullOrEmpty()) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append("Error Message for ")
|
||||
withStyle(style = SpanStyle(color = Color.Green)) {
|
||||
append(userName)
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Detailed explanation for the error
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = if (checkedOutTime.isNotEmpty()) 16.dp else 0.dp)
|
||||
) {
|
||||
Text(
|
||||
letterSpacing = 2.sp,
|
||||
lineHeight = 40.sp,
|
||||
text = detailedExplanation,
|
||||
fontSize = 18.sp,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (checkedOutTime.isNotEmpty()) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Check Out Time",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = checkedOutTime,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.Green
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun LoadingOverlay() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary, modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
||||
|
||||
|
||||
@Composable
|
||||
fun NetworkErrorDialog(
|
||||
showDialog: Boolean,
|
||||
onRetry: () -> Unit,
|
||||
onCheckForUpdate: () -> Unit
|
||||
) {
|
||||
if (!showDialog) return
|
||||
|
||||
var showSettingDialog by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { /* Prevent dismissing by tapping outside */ },
|
||||
title = {
|
||||
Text(
|
||||
text = if (showSettingDialog) "Settings" else "Network Error",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
text = {
|
||||
if (showSettingDialog) {
|
||||
AboutSettings(context = context, onCheckForUpdate)
|
||||
} else {
|
||||
Text(
|
||||
text = "Network is not connected. Please check your Wi-Fi or mobile data.\n\nIf Wi-Fi is just connected, wait for a while and then press retry.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (!showSettingDialog) {
|
||||
Button(onClick = onRetry) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showSettingDialog = !showSettingDialog }) {
|
||||
Text(text = if (showSettingDialog) "Network" else "Settings")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
|
||||
@Composable
|
||||
fun QRCodeScannerScreen(
|
||||
onQRCodeScanned: (String) -> Unit,
|
||||
lifecycleOwner: LifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val previewView = remember { PreviewView(context) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder().build().apply {
|
||||
setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
)
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { analyzer ->
|
||||
analyzer.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
|
||||
processImageProxy(imageProxy, barcodeScanner) { value ->
|
||||
onQRCodeScanned(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_FRONT_CAMERA,
|
||||
preview,
|
||||
imageAnalyzer
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("QRCodeScanner", "Camera binding failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()
|
||||
,
|
||||
factory = { previewView }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun processImageProxy(
|
||||
imageProxy: ImageProxy,
|
||||
scanner: com.google.mlkit.vision.barcode.BarcodeScanner,
|
||||
onQRCodeScanned: (String) -> Unit
|
||||
) {
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null) {
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
barcodes.firstOrNull()?.rawValue?.let { qrCodeValue ->
|
||||
onQRCodeScanned(qrCodeValue)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
Log.e("QRCodeScanner", "Barcode processing failed", it)
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
} else {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun ScannerContent(
|
||||
onQrCodeScanned: (String) -> Unit, onClose: () -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
QRCodeScannerScreen(onQrCodeScanned)
|
||||
IconButton(
|
||||
onClick = onClose, modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Close Scanner")
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler { onClose() }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ScannerToggleButton(
|
||||
isScanning: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier
|
||||
) {
|
||||
OutlinedCard(
|
||||
onClick = onClick, colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
), modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.KeyboardArrowUp,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = if (isScanning) "Close Scanner" else "Open Scanner",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import `in`.sminnovations.attendanceapp.utils.UpdateInfo
|
||||
@Composable
|
||||
fun UpdateDialog(
|
||||
updateInfo: UpdateInfo,
|
||||
downloadProgress: Float,
|
||||
onDismiss: () -> Unit,
|
||||
onInstall: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isinstallPressed = remember { mutableStateOf(false) } //not a smart approach but it works
|
||||
AlertDialog(
|
||||
|
||||
onDismissRequest = {
|
||||
// Only allow dismissing if not downloading
|
||||
if (downloadProgress == 0f) onDismiss()
|
||||
},
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = "Update Available",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "A new version (${updateInfo.version}) is available. Please update.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Release Note:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = updateInfo.releaseNote,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
if (downloadProgress > 0f) {
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row (verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center){
|
||||
LinearProgressIndicator(progress = downloadProgress , modifier = Modifier.fillMaxWidth(0.8f))
|
||||
Text(text = " ${floatToProgress(downloadProgress)}%")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
onInstall(updateInfo.url)
|
||||
isinstallPressed.value = true },
|
||||
enabled = downloadProgress == 0f || downloadProgress == 1f
|
||||
) {
|
||||
AnimatedVisibility(visible = isinstallPressed.value && downloadProgress ==0f) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.surface)
|
||||
}
|
||||
Text(
|
||||
text = if (downloadProgress > 0f && downloadProgress <1f) "Downloading..." else if (downloadProgress == 1f && isinstallPressed.value) "ReInstall Again" else "Install",
|
||||
modifier = Modifier.padding(start = if(isinstallPressed.value && downloadProgress ==0f) 10.dp else 0.dp )
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
if (downloadProgress == 0f) {
|
||||
Button(onClick = onDismiss) {
|
||||
Text("Later")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun floatToProgress(float: Float): Int{
|
||||
return (float * 100f).toInt()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
internal fun RequestPermissionsContent(cameraPermissionState: PermissionState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Camera Permission Required",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
|
||||
Text("Request Permission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.AttendanceScreenContent
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.NetworkErrorDialog
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.UpdateDialog
|
||||
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreenDialogue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
viewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||
val updateInfo by viewModel.updateInfo.collectAsState()
|
||||
val isUpdateDialogVisible by viewModel.isUpdateDialogVisible.collectAsState()
|
||||
val showSettingDialog = remember { mutableStateOf(false) }
|
||||
|
||||
val downloadProgress by viewModel.downloadProgress.collectAsState()
|
||||
val context = LocalContext.current
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
fun checkForUpdateToast(context: Context ,value : Boolean){
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
}
|
||||
if (value) {
|
||||
Toast.makeText(context, "App is Upto Date", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
}
|
||||
SettingsScreenDialogue(showDialogue = showSettingDialog.value,
|
||||
onDismissDialogue = { showSettingDialog.value = false },
|
||||
onCheckForUpdate = {
|
||||
viewModel.checkForUpdates(context)
|
||||
checkForUpdateToast(context,!isUpdateDialogVisible)
|
||||
})
|
||||
|
||||
|
||||
if (uiState.isConnected) {
|
||||
viewModel.checkForUpdates(context)
|
||||
|
||||
if (isUpdateDialogVisible && updateInfo != null) {
|
||||
UpdateDialog(updateInfo = updateInfo!!,
|
||||
downloadProgress = downloadProgress,
|
||||
onDismiss = { viewModel.dismissUpdate() },
|
||||
onInstall = { url -> viewModel.installUpdate(url) })
|
||||
}
|
||||
AttendanceScreenContent(uiState = uiState,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
|
||||
onQrCodeScanned = viewModel::setQRCodeValue,
|
||||
onClickSettings = { showSettingDialog.value = true })
|
||||
|
||||
} else {
|
||||
NetworkErrorDialog(showDialog = true, onRetry = {
|
||||
viewModel.reStartMonitoring()
|
||||
}, onCheckForUpdate = {
|
||||
viewModel.checkForUpdates(context)
|
||||
checkForUpdateToast(context,!isUpdateDialogVisible)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.firebase.Timestamp
|
||||
import com.google.firebase.firestore.DocumentSnapshot
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.home.data.AttendanceUiState
|
||||
import `in`.sminnovations.attendanceapp.utils.AppUpdater
|
||||
import `in`.sminnovations.attendanceapp.utils.NetworkMonitor
|
||||
import `in`.sminnovations.attendanceapp.utils.UpdateChecker
|
||||
import `in`.sminnovations.attendanceapp.utils.UpdateInfo
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
const val timeFormat = "dd MMMM yyyy hh:mm:ss a"
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
applicationContext: Context,
|
||||
private val firebaseFirestore: FirebaseFirestore,
|
||||
private val networkMonitor: NetworkMonitor
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(AttendanceUiState())
|
||||
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null)
|
||||
val updateInfo = _updateInfo.asStateFlow()
|
||||
|
||||
private val _isUpdateDialogVisible = MutableStateFlow(false)
|
||||
val isUpdateDialogVisible = _isUpdateDialogVisible.asStateFlow()
|
||||
|
||||
|
||||
private val _downloadProgress = MutableStateFlow(0f)
|
||||
val downloadProgress: StateFlow<Float> = _downloadProgress
|
||||
|
||||
private val appUpdater = AppUpdater(applicationContext)
|
||||
fun checkForUpdates(context: Context) {
|
||||
viewModelScope.launch {
|
||||
UpdateChecker.checkForUpdate(context).onSuccess { updateInfo ->
|
||||
if (updateInfo.isNew) {
|
||||
_updateInfo.value = updateInfo
|
||||
_isUpdateDialogVisible.value = true
|
||||
}
|
||||
}.onFailure { _ ->
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun installUpdate(url: String) {
|
||||
// Start the download
|
||||
appUpdater.downloadAndInstall(url)
|
||||
|
||||
viewModelScope.launch {
|
||||
appUpdater.updateState.collect { state ->
|
||||
when (state) {
|
||||
is AppUpdater.UpdaterState.Downloading -> {
|
||||
_downloadProgress.value = state.progress
|
||||
}
|
||||
|
||||
is AppUpdater.UpdaterState.Success -> {
|
||||
_downloadProgress.value = 1f
|
||||
}
|
||||
|
||||
is AppUpdater.UpdaterState.Error -> {
|
||||
_downloadProgress.value = 0f
|
||||
}
|
||||
|
||||
AppUpdater.UpdaterState.Idle -> {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun dismissUpdate() {
|
||||
_isUpdateDialogVisible.value = false
|
||||
_updateInfo.value = null
|
||||
_downloadProgress.value = 0f
|
||||
}
|
||||
|
||||
init {
|
||||
networkMonitor.startMonitoring()
|
||||
viewModelScope.launch {
|
||||
networkMonitor.isConnected.asFlow().collectLatest { isConnected ->
|
||||
_uiState.update { it.copy(isConnected = isConnected) }
|
||||
if (isConnected) {
|
||||
startScanning()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun reStartMonitoring() {
|
||||
networkMonitor.startMonitoring()
|
||||
}
|
||||
|
||||
|
||||
fun startScanning() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { _ ->
|
||||
AttendanceUiState( // Reset to completely fresh state
|
||||
isScanning = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stopScanning() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
isScanning = false, errorMessage = null, qrCodeValue = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setQRCodeValue(value: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
qrCodeValue = value,
|
||||
userName = null, // Reset userName when new QR code is scanned
|
||||
errorMessage = null
|
||||
)
|
||||
}
|
||||
stopScanning()
|
||||
processQRCode(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processQRCode(userId: String) {
|
||||
if (userId.isBlank()) return
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
setLoading(true)
|
||||
_uiState.update {
|
||||
it.copy(errorMessage = null, userName = null) // Reset UI state
|
||||
}
|
||||
|
||||
// Fetch Employee Document
|
||||
val employeeDoc = getEmployeeDocument(userId)
|
||||
delay(200L)
|
||||
if (employeeDoc == null) {
|
||||
handleError("Invalid employee ID")
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (employeeDoc.getBoolean("EmployeeStatus") != true) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
userName = employeeDoc.getString("Name"),
|
||||
errorMessage = "Employee ID has expired"
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(userName = employeeDoc.getString("Name")) }
|
||||
|
||||
// Fetch Attendance Document
|
||||
val currentDate = getCurrentFormattedDate()
|
||||
val attendanceDoc = getAttendanceDocument(userId, currentDate)
|
||||
|
||||
if (attendanceDoc?.exists() == true) {
|
||||
handleExistingAttendance(attendanceDoc)
|
||||
_uiState.update { it.copy(isCheckedOut = true, qrCodeValue = null) }
|
||||
} else {
|
||||
handleNewAttendance(userId, currentDate)
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
handleError("An error occurred: ${e.message}")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleError(message: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
errorMessage = message,
|
||||
isLoading = false,
|
||||
userName = null // Reset userName on error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
|
||||
return try {
|
||||
val querySnapshot =
|
||||
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get().await()
|
||||
|
||||
if (querySnapshot.isEmpty) null else querySnapshot.documents.first()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to fetch employee data")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAttendanceDocument(userId: String, date: String): DocumentSnapshot? {
|
||||
return try {
|
||||
firebaseFirestore.collection("attendance_$userId").document(date).get().await()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to fetch attendance data")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setLoading(loading: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = loading) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentFormattedDate(): String {
|
||||
return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
|
||||
}
|
||||
|
||||
private fun formatTimestamp(timestamp: Timestamp): String {
|
||||
return SimpleDateFormat(
|
||||
timeFormat, Locale.getDefault()
|
||||
).format(timestamp.toDate())
|
||||
}
|
||||
|
||||
|
||||
// fun isAppDisabled(): Boolean {
|
||||
// val a = firebaseFirestore.collection("deviceState").document("state")
|
||||
// println(a)
|
||||
// return true
|
||||
// }
|
||||
|
||||
|
||||
private suspend fun handleExistingAttendance(document: DocumentSnapshot) {
|
||||
val data = document.data ?: return
|
||||
val checkIns = data.filterKeys { it.startsWith("checkIn") }.size
|
||||
val checkOuts = data.filterKeys { it.startsWith("checkOut") }.size
|
||||
|
||||
delay(200L)
|
||||
|
||||
val currentTimestamp = formatTimestamp(Timestamp.now())
|
||||
|
||||
val checkIn = document.getString("checkIn$checkIns") ?: run {
|
||||
handleError("Missing check-in data")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (isTimeDifferenceGreaterThanSec(givenTimeStr = checkIn, currentTimeStr = currentTimestamp, secVal = 5L)) {
|
||||
if (!isTimeDifferenceGreaterThanMinutes(givenTimeStr = checkIn, currentTimeStr = currentTimestamp, minutesVal = 5L)) {
|
||||
handleError("Wait 5 minutes before logging off")
|
||||
return
|
||||
} else {
|
||||
if (checkIns > checkOuts) {
|
||||
processCheckOut(document, checkIns)
|
||||
} else {
|
||||
processCheckIn(document, checkIns + 1)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun isTimeDifferenceGreaterThanMinutes(
|
||||
givenTimeStr: String,
|
||||
currentTimeStr: String = formatTimestamp(Timestamp.now()),
|
||||
minutesVal: Long
|
||||
): Boolean {
|
||||
val formatter = DateTimeFormatter.ofPattern(timeFormat)
|
||||
|
||||
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
|
||||
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
|
||||
|
||||
val timeDifference = abs(ChronoUnit.MINUTES.between(givenTime, currentTime))
|
||||
|
||||
return timeDifference > minutesVal
|
||||
}
|
||||
|
||||
|
||||
private fun isTimeDifferenceGreaterThanSec(
|
||||
givenTimeStr: String,
|
||||
currentTimeStr: String = formatTimestamp(Timestamp.now()),
|
||||
secVal: Long
|
||||
): Boolean {
|
||||
val formatter = DateTimeFormatter.ofPattern(timeFormat)
|
||||
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
|
||||
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
|
||||
|
||||
val timeDifference = abs(ChronoUnit.SECONDS.between(givenTime, currentTime))
|
||||
|
||||
return timeDifference > secVal
|
||||
}
|
||||
|
||||
private suspend fun handleNewAttendance(userId: String, currentDate: String) {
|
||||
try {
|
||||
val checkInData = hashMapOf(
|
||||
"DayCompleted" to false,
|
||||
"checkIn1" to formatTimestamp(Timestamp.now()),
|
||||
"totalWork" to "00:00:00"
|
||||
)
|
||||
|
||||
firebaseFirestore.collection("attendance_$userId").document(currentDate)
|
||||
.set(checkInData).await()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCheckedIn = true, qrCodeValue = null
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handleError("Failed to check in: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun processCheckIn(document: DocumentSnapshot, checkInCount: Int) {
|
||||
try {
|
||||
document.reference.update("checkIn$checkInCount", formatTimestamp(Timestamp.now())).await()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCheckedIn = true, qrCodeValue = null
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
handleError("Failed to check in: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processCheckOut(document: DocumentSnapshot, checkInCount: Int) {
|
||||
try {
|
||||
val checkOutTime = formatTimestamp(Timestamp.now())
|
||||
document.reference.update("checkOut$checkInCount", checkOutTime).await()
|
||||
val updatedDocument = document.reference.get().await()
|
||||
val totalWork = calculateTotalWork(updatedDocument)
|
||||
document.reference.update("totalWork", totalWork.first).await()
|
||||
document.reference.update("DayCompleted", totalWork.second).await()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCheckedOut = true, qrCodeValue = null
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
handleError("Failed to check out: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateTotalWork(document: DocumentSnapshot): Pair<String, Boolean> {
|
||||
val formatter = DateTimeFormatter.ofPattern(timeFormat, Locale.getDefault())
|
||||
var totalMinutes = 0L
|
||||
for (i in 1..20) {
|
||||
val checkIn = document.getString("checkIn$i") ?: break
|
||||
val checkOut = document.getString("checkOut$i") ?: continue
|
||||
val checkInTime = LocalDateTime.parse(checkIn, formatter)
|
||||
val checkOutTime = LocalDateTime.parse(checkOut, formatter)
|
||||
totalMinutes += ChronoUnit.MINUTES.between(checkInTime, checkOutTime)
|
||||
}
|
||||
|
||||
val hours = totalMinutes / 60
|
||||
val minutes = totalMinutes % 60
|
||||
val isDayCompleted = totalMinutes >= 420L
|
||||
return Pair(String.format("%02d:%02d:00", hours, minutes), isDayCompleted)
|
||||
}
|
||||
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
networkMonitor.stopMonitoring()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
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
|
||||
|
||||
|
||||
class SettingsData(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("Settings")
|
||||
val DYNAMIC_THEME_KEY = booleanPreferencesKey("dynamic_theme")
|
||||
val BLACK_THEME_KEY = booleanPreferencesKey("black_theme")
|
||||
val THEME_MODE_KEY = stringPreferencesKey("app_theme")
|
||||
val DEV_MODE_TOGGLE_KEY = booleanPreferencesKey("dev_mode_toggle")
|
||||
}
|
||||
|
||||
|
||||
val getDevModeToggle: Flow<Boolean?> = context.dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[DEV_MODE_TOGGLE_KEY] ?: false
|
||||
}
|
||||
|
||||
suspend fun setDevMode(mode: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[DEV_MODE_TOGGLE_KEY] = mode
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
val getDynamicColorThemeValue: Flow<Boolean?> = context.dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[DYNAMIC_THEME_KEY] ?: true
|
||||
}
|
||||
|
||||
suspend fun setDynamicColorThemeValue(value: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[DYNAMIC_THEME_KEY] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
val getBlackThemeValue: Flow<Boolean?> = context.dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[BLACK_THEME_KEY] ?: false
|
||||
}
|
||||
|
||||
suspend fun setBlackThemeValue(value: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[BLACK_THEME_KEY] = value
|
||||
}
|
||||
}
|
||||
|
||||
val getThemeMode: Flow<String> = context.dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[THEME_MODE_KEY] ?: "System default"
|
||||
}
|
||||
|
||||
suspend fun saveThemeMode(theme: String) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[THEME_MODE_KEY] = theme
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.ui
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
||||
|
||||
|
||||
@Composable
|
||||
fun SettingsScreenDialogue(
|
||||
showDialogue: Boolean,
|
||||
onDismissDialogue: () -> Unit,
|
||||
onCheckForUpdate: () -> Unit,
|
||||
) {
|
||||
if (showDialogue) {
|
||||
val context = LocalContext.current
|
||||
AlertDialog(
|
||||
onDismissRequest = { /* Do nothing to prevent dismissal by tapping outside */ },
|
||||
title = {
|
||||
Text(text = "Settings")
|
||||
},
|
||||
text = {
|
||||
AboutSettings(context = context,onCheckForUpdate)
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
Button(onClick = onDismissDialogue) {
|
||||
Text("Close")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
|
||||
|
||||
@Composable
|
||||
fun TopComponents(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.smi_logo),
|
||||
contentDescription = "Profile Photo",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(150.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun ClickableCard(
|
||||
title:String,
|
||||
subtitle:String,
|
||||
onClick: (Boolean) -> Unit,
|
||||
icon: ImageVector
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
// .background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.clickable{ onClick(true)}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClickableCardNonBoolean(
|
||||
title:String,
|
||||
subtitle:String,
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
// .background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.clickable{ onClick()}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@OptIn( ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun CombinedClickableCard(
|
||||
title:String,
|
||||
subtitle:String,
|
||||
onClick: (Boolean) -> Unit,
|
||||
onDoubleClick: ((Boolean) -> Unit?)? =null,
|
||||
onLongClick: ((Boolean) -> Unit?)? =null,
|
||||
icon: ImageVector
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
// .background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.combinedClickable (
|
||||
onClick = { onClick(true) },
|
||||
onDoubleClick = {
|
||||
if (onDoubleClick != null) {
|
||||
onDoubleClick(true)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (onLongClick != null) {
|
||||
onLongClick(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.indication(interactionSource,null)
|
||||
,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun SwitchCard(
|
||||
checkedValue:Boolean,
|
||||
title:String,
|
||||
subtitle:String,
|
||||
onSwitchChange: (Boolean) -> Unit,
|
||||
icon: ImageVector
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// .background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.toggleable(
|
||||
value = checkedValue,
|
||||
enabled = true,
|
||||
role = Role.Switch,
|
||||
onValueChange = onSwitchChange
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = checkedValue,
|
||||
onCheckedChange = onSwitchChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCardNonBoolean
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.CombinedClickableCard
|
||||
|
||||
|
||||
@Composable
|
||||
fun AboutSettings(
|
||||
context: Context ,
|
||||
onCheckUpdate : () -> Unit
|
||||
) {
|
||||
Column() {
|
||||
OutlinedCard {
|
||||
ClickableCard(
|
||||
title = "Version",
|
||||
subtitle = context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
0
|
||||
).versionName,
|
||||
onClick = {},
|
||||
icon = ImageVector.vectorResource(id = R.drawable.round_info_24)
|
||||
|
||||
)
|
||||
ClickableCardNonBoolean(
|
||||
title = "Check For Update",
|
||||
subtitle = "Check For latest update",
|
||||
onClick = onCheckUpdate,
|
||||
icon = ImageVector.vectorResource(id = R.drawable.round_info_24)
|
||||
|
||||
)
|
||||
CombinedClickableCard(title = "Developer",
|
||||
subtitle = "ShanMukha Innovations Pvt Ltd",
|
||||
onClick = {},
|
||||
onDoubleClick = {
|
||||
|
||||
},
|
||||
icon = ImageVector.vectorResource(id = R.drawable.baseline_developer_mode_24)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package `in`.sminnovations.attendanceapp.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
val Purple80 = Color(0xFFFF9800)
|
||||
val PurpleGrey80 = Color(0xFFFFCB7E)
|
||||
val Pink80 = Color(0xFF9B6D7B)
|
||||
|
||||
val Purple40 = Color(0xFF000000)
|
||||
val PurpleGrey40 = Color(0xFF865307)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -11,7 +11,7 @@ val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package `in`.sminnovations.attendanceapp.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
data class UpdateInfo(
|
||||
val version: String = "",
|
||||
val url: String = "",
|
||||
val isNew: Boolean = false,
|
||||
val releaseNote : String = ""
|
||||
)
|
||||
|
||||
|
||||
class AppUpdater(private val context: Context) {
|
||||
sealed class UpdaterState {
|
||||
data object Idle : UpdaterState()
|
||||
data class Downloading(val progress: Float) : UpdaterState()
|
||||
data class Success(val file: File) : UpdaterState()
|
||||
data class Error(val message: String) : UpdaterState()
|
||||
}
|
||||
|
||||
private val _updateState = MutableStateFlow<UpdaterState>(UpdaterState.Idle)
|
||||
val updateState = _updateState.asStateFlow()
|
||||
|
||||
fun downloadAndInstall(url: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
//Added for future use case
|
||||
// Resolve the URL (e.g. for Google Drive links)
|
||||
val resolvedUrl = resolveDownloadUrl(url)
|
||||
val file = downloadApk(resolvedUrl) { progress ->
|
||||
// Update progress on Main thread
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
_updateState.value = UpdaterState.Downloading(progress)
|
||||
}
|
||||
}
|
||||
_updateState.value = UpdaterState.Success(file)
|
||||
installApk(file)
|
||||
} catch (e: Exception) {
|
||||
_updateState.value = UpdaterState.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDownloadUrl(url: String): String {
|
||||
// Optional
|
||||
// If the URL is from Google Drive, extract file id and convert to direct download URL.
|
||||
return if (url.contains("drive.google.com")) {
|
||||
val regex = Regex("(?<=/d/)[^/]+")
|
||||
val fileId = regex.find(url)?.value ?: Uri.parse(url).getQueryParameter("id")
|
||||
if (!fileId.isNullOrEmpty())
|
||||
"https://drive.google.com/uc?export=download&id=$fileId"
|
||||
else url
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File =
|
||||
withContext(Dispatchers.IO) {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
val contentLength = originalResponse.body?.contentLength() ?: -1L
|
||||
originalResponse.newBuilder()
|
||||
.body(originalResponse.body?.let { body ->
|
||||
ProgressResponseBody(body) { progress ->
|
||||
onProgress(progress)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
val request = Request.Builder().url(url).build()
|
||||
val file = File(context.cacheDir, "update.apk")
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) throw IOException("Failed to download APK")
|
||||
response.body?.let { body ->
|
||||
file.outputStream().use { fileOut ->
|
||||
body.byteStream().use { it.copyTo(fileOut) }
|
||||
}
|
||||
} ?: throw IOException("Empty response")
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
private fun installApk(file: File) {
|
||||
try {
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
||||
if (file.length() == 0L) throw IOException("Downloaded file is empty")
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
throw IOException("No app found to handle installation")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressResponseBody(
|
||||
private val responseBody: ResponseBody,
|
||||
private val onProgressUpdate: (Float) -> Unit
|
||||
) : ResponseBody() {
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
source(responseBody.source()).buffer()
|
||||
}
|
||||
override fun contentType() = responseBody.contentType()
|
||||
override fun contentLength() = responseBody.contentLength()
|
||||
override fun source(): BufferedSource = bufferedSource
|
||||
|
||||
private fun source(source: Source): Source =
|
||||
object : ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
val progress = if (contentLength() > 0) totalBytesRead.toFloat() / contentLength() else -1f
|
||||
onProgressUpdate(progress)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package `in`.sminnovations.attendanceapp.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NetworkMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
private val _isConnected = MutableLiveData(false)
|
||||
val isConnected: LiveData<Boolean> get() = _isConnected
|
||||
|
||||
private val networkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
performPingCheck()
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
_isConnected.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun startMonitoring() {
|
||||
val network = connectivityManager.activeNetwork
|
||||
if (network != null) {
|
||||
performPingCheck()
|
||||
}
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||
}
|
||||
|
||||
fun stopMonitoring() {
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
networkScope.cancel()
|
||||
}
|
||||
|
||||
private fun performPingCheck() {
|
||||
networkScope.launch {
|
||||
try {
|
||||
val url = URL("https://www.google.com")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 2000
|
||||
connection.connect()
|
||||
val isReachable = connection.responseCode == 200
|
||||
_isConnected.postValue(isReachable)
|
||||
connection.disconnect()
|
||||
} catch (e: Exception) {
|
||||
_isConnected.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package `in`.sminnovations.attendanceapp.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.firestore.ktx.firestore
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
|
||||
object UpdateChecker {
|
||||
private const val COLLECTION_NAME = "app_updates"
|
||||
private const val DOCUMENT_NAME = "latest_version"
|
||||
|
||||
suspend fun checkForUpdate(
|
||||
context: Context
|
||||
): Result<UpdateInfo> = runCatching {
|
||||
val currentVersion = getCurrentVersion(context)
|
||||
val updateInfo = getLatestVersion()
|
||||
|
||||
if (isNewerVersion(currentVersion, updateInfo.version)) {
|
||||
updateInfo.copy(isNew = true)
|
||||
} else {
|
||||
updateInfo.copy(isNew = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentVersion(context: Context): String {
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
} catch (e: Exception) {
|
||||
throw UpdateCheckException("Failed to get current app version", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLatestVersion(): UpdateInfo {
|
||||
return try {
|
||||
val document = Firebase.firestore
|
||||
.collection(COLLECTION_NAME)
|
||||
.document(DOCUMENT_NAME)
|
||||
.get()
|
||||
.await()
|
||||
|
||||
if (!document.exists()) {
|
||||
throw UpdateCheckException("Update document does not exist")
|
||||
}
|
||||
|
||||
UpdateInfo(
|
||||
version = document.getString("version") ?: throw UpdateCheckException("Version field missing"),
|
||||
url = document.getString("url") ?: throw UpdateCheckException("URL field missing"),
|
||||
releaseNote = document.getString("ReleaseNote") ?: throw UpdateCheckException("ReleaseNote field missing")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw UpdateCheckException("Failed to fetch update info", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNewerVersion(currentVersion: String, latestVersion: String): Boolean {
|
||||
return try {
|
||||
val current = currentVersion.split(".").map { it.toIntOrNull() ?: 0 }
|
||||
val latest = latestVersion.split(".").map { it.toIntOrNull() ?: 0 }
|
||||
|
||||
for (i in 0 until maxOf(current.size, latest.size)) {
|
||||
val currentNum = current.getOrNull(i) ?: 0
|
||||
val latestNum = latest.getOrNull(i) ?: 0
|
||||
|
||||
when {
|
||||
latestNum > currentNum -> return true
|
||||
latestNum < currentNum -> return false
|
||||
}
|
||||
}
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateCheckException(
|
||||
message: String,
|
||||
cause: Throwable? = null
|
||||
) : Exception(message, cause){
|
||||
|
||||
|
||||
}
|
||||
5
app/src/main/res/drawable/baseline_close_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_developer_mode_24.xml
Normal 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="M7,5h10v2h2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v4h2L7,5zM15.41,16.59L20,12l-4.59,-4.59L14,8.83 17.17,12 14,15.17l1.41,1.42zM10,15.17L6.83,12 10,8.83 8.59,7.41 4,12l4.59,4.59L10,15.17zM17,19L7,19v-2L5,17v4c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-4h-2v2z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_settings_24.xml
Normal 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.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/round_delete_sweep_24.xml
Normal 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="M16,16h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1zM16,8h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1zM16,12h4c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-4c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM13,5h-2l-0.71,-0.71c-0.18,-0.18 -0.44,-0.29 -0.7,-0.29L6.41,4c-0.26,0 -0.52,0.11 -0.7,0.29L5,5L3,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h10c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/round_info_24.xml
Normal 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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,17c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v4c0,0.55 -0.45,1 -1,1zM13,9h-2L11,7h2v2z"/>
|
||||
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/smi_logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 926 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 9.7 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#EBFFF3</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,14 @@
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name">SMI Attendance</string>
|
||||
<string name="cancel" tools:ignore="ButtonCase">CANCEL</string>
|
||||
<string name="dynamic_theme">Dynamic Theme</string>
|
||||
<string name="developer_options">Developer Options</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="stats">Stats</string>
|
||||
<string name="theme_based_on_wallpaper_color">Theme based on wallpaper color</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="select_theme_mode">Select Theme Mode</string>
|
||||
<string name="black_theme">Black Theme</string>
|
||||
<string name="black_theme_description">Pitch Black Theme</string>
|
||||
</resources>
|
||||
6
app/src/main/res/xml/provider_paths.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="updates"
|
||||
path="." />
|
||||
</paths>
|
||||
@@ -1,8 +1,14 @@
|
||||
package `in`.sminnovations.attendanceapp
|
||||
|
||||
import com.google.firebase.Timestamp
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
@@ -10,8 +16,48 @@ import org.junit.Assert.*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
/*
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timeTest(){
|
||||
val currentTime = "30 January 2025 10:30:55 am"
|
||||
val givenTime = "30 January 2025 10:31:01 am"
|
||||
val minutes = 7L
|
||||
val result = isTimeDifferenceGreaterThanSec(givenTime, currentTime, minutes)
|
||||
println(result)
|
||||
assertEquals(false,result)
|
||||
}
|
||||
//
|
||||
// private fun isTimeDifferenceGreaterThanMinutes(givenTimeStr: String, currentTimeStr: String, minutes: Long): Boolean {
|
||||
// val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy hh:mm:ss a")
|
||||
// val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
|
||||
// val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
|
||||
// val timeDifference = abs(ChronoUnit.MINUTES.between(givenTime, currentTime))
|
||||
// return timeDifference > minutes
|
||||
// }
|
||||
|
||||
private fun isTimeDifferenceGreaterThanSec(
|
||||
givenTimeStr: String,
|
||||
currentTimeStr: String = formatTimestamp(Timestamp.now()),
|
||||
sec: Long = 5
|
||||
): Boolean {
|
||||
val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy hh:mm:ss a")
|
||||
val givenTime = LocalDateTime.parse(givenTimeStr, formatter)
|
||||
val currentTime = LocalDateTime.parse(currentTimeStr, formatter)
|
||||
|
||||
val timeDifference = abs(ChronoUnit.SECONDS.between(givenTime, currentTime))
|
||||
|
||||
return timeDifference > sec
|
||||
}
|
||||
|
||||
private fun formatTimestamp(timestamp: Timestamp): String {
|
||||
return SimpleDateFormat(
|
||||
"dd MMMM yyyy hh:mm:ss a", Locale.getDefault()
|
||||
).format(timestamp.toDate())
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -2,4 +2,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
}
|
||||
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.4.0"
|
||||
agp = "8.5.0"
|
||||
kotlin = "1.9.0"
|
||||
coreKtx = "1.13.1"
|
||||
junit = "4.13.2"
|
||||
@@ -8,6 +8,8 @@ espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.6"
|
||||
activityCompose = "1.9.2"
|
||||
composeBom = "2023.08.00"
|
||||
datastoreCoreAndroid = "1.1.1"
|
||||
datastorePreferencesCoreJvm = "1.1.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -24,6 +26,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastoreCoreAndroid" }
|
||||
androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name = "datastore-preferences-core-jvm", version.ref = "datastorePreferencesCoreJvm" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Fri Oct 04 14:44:43 IST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -16,8 +16,10 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rootProject.name = "SMI Attendance"
|
||||
include(":app")
|
||||
|
||||