Compare commits
97 Commits
dev-sacnne
...
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 |
10
.idea/deploymentTargetSelector.xml
generated
10
.idea/deploymentTargetSelector.xml
generated
@@ -4,21 +4,15 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2024-10-16T06:25:13.029795100Z">
|
<DropdownSelection timestamp="2025-02-06T09:50:04.018268400Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sathw\.android\avd\Pixel_3a_XL_API_34.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=R9ZW605ZHAY" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
<DialogSelection />
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="lofpre">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="checkTime">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="AnimatedPreloader">
|
<SelectionState runConfigName="AnimatedPreloader">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
|
|||||||
209
.idea/other.xml
generated
209
.idea/other.xml
generated
@@ -14,6 +14,17 @@
|
|||||||
<option name="screenX" value="720" />
|
<option name="screenX" value="720" />
|
||||||
<option name="screenY" value="1280" />
|
<option name="screenY" value="1280" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="OPPO" />
|
<option name="brand" value="OPPO" />
|
||||||
@@ -47,6 +58,28 @@
|
|||||||
<option name="screenX" value="1840" />
|
<option name="screenX" value="1840" />
|
||||||
<option name="screenY" value="2944" />
|
<option name="screenY" value="2944" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="31" />
|
<option name="api" value="31" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -69,6 +102,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -124,6 +179,17 @@
|
|||||||
<option name="screenX" value="2220" />
|
<option name="screenX" value="2220" />
|
||||||
<option name="screenY" value="1080" />
|
<option name="screenY" value="1080" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -146,6 +212,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2340" />
|
<option name="screenY" value="2340" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -179,6 +267,50 @@
|
|||||||
<option name="screenX" value="2208" />
|
<option name="screenX" value="2208" />
|
||||||
<option name="screenY" value="1840" />
|
<option name="screenY" value="1840" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -190,6 +322,28 @@
|
|||||||
<option name="screenX" value="1848" />
|
<option name="screenX" value="1848" />
|
||||||
<option name="screenY" value="2960" />
|
<option name="screenY" value="2960" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -234,6 +388,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="31" />
|
<option name="api" value="31" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -290,6 +466,17 @@
|
|||||||
<option name="screenY" value="384" />
|
<option name="screenY" value="384" />
|
||||||
<option name="type" value="WEAR_OS" />
|
<option name="type" value="WEAR_OS" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="30" />
|
<option name="api" value="30" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -312,6 +499,17 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -334,6 +532,17 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2424" />
|
<option name="screenY" value="2424" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ plugins {
|
|||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
alias(libs.plugins.jetbrains.kotlin.android)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -15,9 +14,9 @@ android {
|
|||||||
applicationId = "in.sminnovations.attendanceapp.user"
|
applicationId = "in.sminnovations.attendanceapp.user"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 4
|
versionCode = 11
|
||||||
|
|
||||||
versionName = "3.0.4"
|
versionName = "3.1.5"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -27,7 +26,7 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -106,7 +105,7 @@ dependencies {
|
|||||||
implementation("com.google.firebase:firebase-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-ktx:25.1.0") // Use the latest version
|
||||||
implementation("com.google.firebase:firebase-firestore")
|
implementation("com.google.firebase:firebase-firestore")
|
||||||
|
implementation ("com.squareup.okhttp3:okhttp:4.11.0")
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,14 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<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" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<application
|
<application
|
||||||
@@ -28,6 +36,15 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -7,7 +7,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import `in`.sminnovations.attendanceapp.main.NetworkMonitor
|
import `in`.sminnovations.attendanceapp.utils.NetworkMonitor
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import android.app.Application
|
|||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class MyApp : Application(){
|
class MyApp : Application()
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,23 +5,16 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.compose.rememberNavController
|
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
|
||||||
import `in`.sminnovations.attendanceapp.navigation.BottomNavigationBar
|
|
||||||
import `in`.sminnovations.attendanceapp.navigation.Navigation
|
|
||||||
import `in`.sminnovations.attendanceapp.navigation.data.navItems
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
|
||||||
BottomNavigationBar(navController, navItems)
|
|
||||||
},
|
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Box(modifier = Modifier.padding(padding)) {
|
Box(modifier = Modifier.padding(padding)) {
|
||||||
Navigation(navController = navController, items = navItems)
|
HomeScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.navigation
|
|
||||||
|
|
||||||
import androidx.compose.material3.NavigationBar
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BottomNavigationBar(
|
|
||||||
navController: NavController,
|
|
||||||
items: List<BottomNavItem>,
|
|
||||||
) {
|
|
||||||
NavigationBar {
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
|
||||||
items.forEach { item ->
|
|
||||||
NavigationBarItem(icon = {
|
|
||||||
BottomNavAnimatedIcon(
|
|
||||||
isSelected = currentRoute == item.route,
|
|
||||||
icon = item.icon,
|
|
||||||
selectedIcon = item.selectedIcon,
|
|
||||||
contentDescription = item.label
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(text = item.label) },
|
|
||||||
alwaysShowLabel = true,
|
|
||||||
selected = currentRoute == item.route,
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(item.route) {
|
|
||||||
navController.graph.startDestinationRoute?.let { route ->
|
|
||||||
popUpTo(route) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.navigation
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BottomNavAnimatedIcon(
|
|
||||||
isSelected: Boolean,
|
|
||||||
icon: ImageVector,
|
|
||||||
selectedIcon: ImageVector,
|
|
||||||
contentDescription: String?
|
|
||||||
) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = isSelected,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(animationSpec = tween(300)) togetherWith fadeOut(animationSpec = tween(300))
|
|
||||||
}, label = "animated icon"
|
|
||||||
) { targetState ->
|
|
||||||
Icon(
|
|
||||||
imageVector = if (targetState) selectedIcon else icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = if (targetState) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.navigation
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
|
|
||||||
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreen
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Navigation(
|
|
||||||
navController: NavHostController,
|
|
||||||
items: List<BottomNavItem>,
|
|
||||||
) {
|
|
||||||
NavHost(navController, startDestination = items[0].route) {
|
|
||||||
composable(items[0].route) {
|
|
||||||
HomeScreen()
|
|
||||||
}
|
|
||||||
composable(items[1].route) {
|
|
||||||
SettingsScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.navigation.data
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.AddCircle
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.AddCircle
|
|
||||||
import androidx.compose.material.icons.outlined.Search
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
|
|
||||||
|
|
||||||
data class BottomNavItem(
|
|
||||||
val icon: ImageVector,
|
|
||||||
val selectedIcon: ImageVector,
|
|
||||||
val label: String,
|
|
||||||
val route: String
|
|
||||||
)
|
|
||||||
val navItems = listOf(
|
|
||||||
BottomNavItem(Icons.Outlined.Search, Icons.Filled.Search, "Scan", "scan"),
|
|
||||||
BottomNavItem(Icons.Outlined.Settings, Icons.Filled.Settings, "Settings", "settings")
|
|
||||||
)
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -1,41 +1,20 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
import com.google.accompanist.permissions.PermissionState
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import `in`.sminnovations.attendanceapp.R
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.viewModel.AttendanceUiState
|
import `in`.sminnovations.attendanceapp.screens.home.data.AttendanceUiState
|
||||||
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,44 +22,59 @@ internal fun AttendanceScreenContent(
|
|||||||
uiState: AttendanceUiState,
|
uiState: AttendanceUiState,
|
||||||
cameraPermissionState: PermissionState,
|
cameraPermissionState: PermissionState,
|
||||||
onScannerToggle: () -> Unit,
|
onScannerToggle: () -> Unit,
|
||||||
|
onClickSettings: () -> Unit,
|
||||||
onQrCodeScanned: (String) -> Unit,
|
onQrCodeScanned: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
when {
|
IconButton(onClick = onClickSettings) {
|
||||||
!cameraPermissionState.status.isGranted -> {
|
Icon(painter = painterResource(id = R.drawable.baseline_settings_24), contentDescription ="settings" )
|
||||||
RequestPermissionsContent(cameraPermissionState)
|
}
|
||||||
}
|
Column {
|
||||||
uiState.isScanning -> {
|
when {
|
||||||
ScannerContent(
|
!cameraPermissionState.status.isGranted -> {
|
||||||
onQrCodeScanned = onQrCodeScanned,
|
RequestPermissionsContent(cameraPermissionState)
|
||||||
onClose = onScannerToggle
|
}
|
||||||
)
|
|
||||||
}
|
uiState.isScanning -> {
|
||||||
uiState.errorMessage != null -> {
|
ScannerContent(
|
||||||
ErrorContent(
|
onQrCodeScanned = onQrCodeScanned, onClose = onScannerToggle
|
||||||
errorMessage = uiState.errorMessage,
|
)
|
||||||
userName = uiState.userName,
|
}
|
||||||
checkedOutTime = uiState.checkedOutTime
|
|
||||||
)
|
uiState.errorMessage != null -> {
|
||||||
}
|
ErrorContent(
|
||||||
uiState.isCheckedIn -> {
|
errorMessage = uiState.errorMessage,
|
||||||
AttendanceStatusCard(
|
userName = uiState.userName,
|
||||||
title = "Checked In as",
|
checkedOutTime = uiState.checkedOutTime
|
||||||
userName = uiState.userName
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
uiState.isCheckedOut -> {
|
uiState.isCheckedIn -> {
|
||||||
AttendanceStatusCard(
|
AttendanceStatusCard(
|
||||||
title = "Checked Out as",
|
title = "Checked In as", userName = uiState.userName
|
||||||
userName = uiState.userName
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
uiState.isCheckedOut -> {
|
||||||
|
AttendanceStatusCard(
|
||||||
|
title = "Checked Out as", userName = uiState.userName
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
LoadingOverlay()
|
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(
|
ScannerToggleButton(
|
||||||
isScanning = uiState.isScanning,
|
isScanning = uiState.isScanning,
|
||||||
@@ -90,108 +84,3 @@ internal fun AttendanceScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private 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,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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
|
||||||
|
|
||||||
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.unit.dp
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NetworkErrorDialog(
|
|
||||||
showDialog: Boolean,
|
|
||||||
onRetry: () -> Unit
|
|
||||||
) {
|
|
||||||
if (showDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { /* Do nothing to prevent dismissal by tapping outside */ },
|
|
||||||
title = {
|
|
||||||
Text(text = "Network Error")
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(text = "Network is not connected. Please check your Wi-Fi or mobile data.\n\nIf Wi-Fi is just connected wait for some time and press retry again")
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onRetry()
|
|
||||||
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Retry")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ internal fun ErrorContent(
|
|||||||
"Failed to check out" to "There was an issue recording your check-out. 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
|
// Generic Error
|
||||||
"An error occurred" to "An unexpected error occurred. Please try again or contact support if the issue continues."
|
"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 😊"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ fun QRCodeScannerScreen(
|
|||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight(.84f)
|
.fillMaxHeight()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
,
|
,
|
||||||
factory = { previewView }
|
factory = { previewView }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,39 +1,91 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui
|
package `in`.sminnovations.attendanceapp.screens.home.ui
|
||||||
|
|
||||||
import android.Manifest
|
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.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.AttendanceScreenContent
|
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.NetworkErrorDialog
|
||||||
|
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.UpdateDialog
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
|
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)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
viewModel: HomeViewModel = hiltViewModel(),
|
viewModel: HomeViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||||
NetworkErrorDialog(
|
val updateInfo by viewModel.updateInfo.collectAsState()
|
||||||
showDialog = !uiState.isConnected,
|
val isUpdateDialogVisible by viewModel.isUpdateDialogVisible.collectAsState()
|
||||||
onRetry = {
|
val showSettingDialog = remember { mutableStateOf(false) }
|
||||||
viewModel.reStartMonitoring()
|
|
||||||
|
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,
|
||||||
if (uiState.isConnected){
|
onDismissDialogue = { showSettingDialog.value = false },
|
||||||
AttendanceScreenContent(
|
onCheckForUpdate = {
|
||||||
uiState = uiState,
|
viewModel.checkForUpdates(context)
|
||||||
cameraPermissionState = cameraPermissionState,
|
checkForUpdateToast(context,!isUpdateDialogVisible)
|
||||||
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
|
})
|
||||||
onQrCodeScanned = viewModel::setQRCodeValue,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
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)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
|||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -12,23 +9,33 @@ import com.google.firebase.Timestamp
|
|||||||
import com.google.firebase.firestore.DocumentSnapshot
|
import com.google.firebase.firestore.DocumentSnapshot
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import `in`.sminnovations.attendanceapp.main.NetworkMonitor
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import java.text.SimpleDateFormat
|
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.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
|
const val timeFormat = "dd MMMM yyyy hh:mm:ss a"
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
private val applicationContext: Context,
|
applicationContext: Context,
|
||||||
private val firebaseFirestore: FirebaseFirestore,
|
private val firebaseFirestore: FirebaseFirestore,
|
||||||
private val networkMonitor: NetworkMonitor
|
private val networkMonitor: NetworkMonitor
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -36,36 +43,88 @@ class HomeViewModel @Inject constructor(
|
|||||||
private val _uiState = MutableStateFlow(AttendanceUiState())
|
private val _uiState = MutableStateFlow(AttendanceUiState())
|
||||||
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null)
|
||||||
|
val updateInfo = _updateInfo.asStateFlow()
|
||||||
|
|
||||||
networkMonitor.startMonitoring()
|
private val _isUpdateDialogVisible = MutableStateFlow(false)
|
||||||
networkMonitor.isConnected.asFlow()
|
val isUpdateDialogVisible = _isUpdateDialogVisible.asStateFlow()
|
||||||
.onEach { isConnected ->
|
|
||||||
_uiState.update { currentState ->
|
|
||||||
currentState.copy(isConnected = isConnected)
|
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 { _ ->
|
||||||
.launchIn(viewModelScope)
|
// Handle error
|
||||||
if (uiState.value.isConnected) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
startScanning()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopMonitoring() {
|
|
||||||
networkMonitor.stopMonitoring()
|
|
||||||
|
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() {
|
fun reStartMonitoring() {
|
||||||
networkMonitor.stopMonitoring()
|
|
||||||
networkMonitor.startMonitoring()
|
networkMonitor.startMonitoring()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun startScanning() {
|
fun startScanning() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { currentState ->
|
_uiState.update { _ ->
|
||||||
AttendanceUiState( // Reset to completely fresh state
|
AttendanceUiState( // Reset to completely fresh state
|
||||||
isScanning = true
|
isScanning = true
|
||||||
)
|
)
|
||||||
@@ -100,24 +159,22 @@ class HomeViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun processQRCode(userId: String) {
|
private fun processQRCode(userId: String) {
|
||||||
if (userId.isBlank()) return
|
if (userId.isBlank()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(errorMessage = null, userName = null) // Reset UI state
|
||||||
errorMessage = null, userName = null // Reset userName before processing
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch Employee Document
|
||||||
val employeeDoc = getEmployeeDocument(userId)
|
val employeeDoc = getEmployeeDocument(userId)
|
||||||
|
delay(200L)
|
||||||
if (employeeDoc == null) {
|
if (employeeDoc == null) {
|
||||||
handleError("Invalid employee ID")
|
handleError("Invalid employee ID")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employeeDoc.getBoolean("EmployeeStatus")!!) {
|
if (employeeDoc.getBoolean("EmployeeStatus") != true) {
|
||||||
// Set userName only for expired ID case
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
userName = employeeDoc.getString("Name"),
|
userName = employeeDoc.getString("Name"),
|
||||||
@@ -129,15 +186,19 @@ class HomeViewModel @Inject constructor(
|
|||||||
|
|
||||||
_uiState.update { it.copy(userName = employeeDoc.getString("Name")) }
|
_uiState.update { it.copy(userName = employeeDoc.getString("Name")) }
|
||||||
|
|
||||||
|
// Fetch Attendance Document
|
||||||
val currentDate = getCurrentFormattedDate()
|
val currentDate = getCurrentFormattedDate()
|
||||||
val attendanceDoc = getAttendanceDocument(userId, currentDate)
|
val attendanceDoc = getAttendanceDocument(userId, currentDate)
|
||||||
|
|
||||||
if (attendanceDoc?.exists() == true) {
|
if (attendanceDoc?.exists() == true) {
|
||||||
handleExistingAttendance(userId, attendanceDoc)
|
handleExistingAttendance(attendanceDoc)
|
||||||
|
_uiState.update { it.copy(isCheckedOut = true, qrCodeValue = null) }
|
||||||
} else {
|
} else {
|
||||||
handleNewAttendance(userId, currentDate)
|
handleNewAttendance(userId, currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleError("An error occurred: ${e.message}")
|
handleError("An error occurred: ${e.message}")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -146,6 +207,7 @@ class HomeViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleError(message: String) {
|
private fun handleError(message: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -158,61 +220,6 @@ class HomeViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleExistingAttendance(userId: String, document: DocumentSnapshot) {
|
|
||||||
val checkOut = document.getString("checkOut")
|
|
||||||
|
|
||||||
when {
|
|
||||||
checkOut == "NA" -> processCheckOut(userId, document)
|
|
||||||
checkOut != null -> {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
checkedOutTime = checkOut, errorMessage = "Already checked out for today."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> handleError("Invalid attendance record")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleNewAttendance(userId: String, currentDate: String) {
|
|
||||||
try {
|
|
||||||
val checkInData = hashMapOf(
|
|
||||||
"DayCompleted" to false,
|
|
||||||
"checkIn" to formatTimestamp(Timestamp.now()),
|
|
||||||
"checkOut" to "NA"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 processCheckOut(userId: String, document: DocumentSnapshot) {
|
|
||||||
try {
|
|
||||||
val checkOutData = hashMapOf(
|
|
||||||
"checkOut" to formatTimestamp(Timestamp.now()), "DayCompleted" to true
|
|
||||||
)
|
|
||||||
|
|
||||||
document.reference.update(checkOutData as Map<String, Any>).await()
|
|
||||||
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isCheckedOut = true, qrCodeValue = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
handleError("Failed to check out: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
|
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
|
||||||
return try {
|
return try {
|
||||||
@@ -246,26 +253,156 @@ class HomeViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun formatTimestamp(timestamp: Timestamp): String {
|
private fun formatTimestamp(timestamp: Timestamp): String {
|
||||||
return SimpleDateFormat(
|
return SimpleDateFormat(
|
||||||
"dd MMMM yyyy hh:mm:ss a",
|
timeFormat, Locale.getDefault()
|
||||||
Locale.getDefault()
|
|
||||||
).format(timestamp.toDate())
|
).format(timestamp.toDate())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun isDeviceConnectedToInternet(context: Context): Boolean {
|
// fun isAppDisabled(): Boolean {
|
||||||
val connectivityManager =
|
// val a = firebaseFirestore.collection("deviceState").document("state")
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
// println(a)
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
val network = connectivityManager.activeNetwork ?: return false
|
|
||||||
val networkCapabilities =
|
private suspend fun handleExistingAttendance(document: DocumentSnapshot) {
|
||||||
connectivityManager.getNetworkCapabilities(network) ?: return false
|
val data = document.data ?: return
|
||||||
return when {
|
val checkIns = data.filterKeys { it.startsWith("checkIn") }.size
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
val checkOuts = data.filterKeys { it.startsWith("checkOut") }.size
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
delay(200L)
|
||||||
else -> false
|
|
||||||
|
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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
networkMonitor.stopMonitoring()
|
networkMonitor.stopMonitoring()
|
||||||
@@ -273,14 +410,3 @@ class HomeViewModel @Inject constructor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,45 +1,41 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui
|
package `in`.sminnovations.attendanceapp.screens.settings.ui
|
||||||
|
|
||||||
|
|
||||||
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.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.Developer_settings
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreenDialogue(
|
||||||
|
showDialogue: Boolean,
|
||||||
|
onDismissDialogue: () -> Unit,
|
||||||
|
onCheckForUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
if (showDialogue) {
|
||||||
val scrollState = rememberScrollState()
|
val context = LocalContext.current
|
||||||
Column(
|
AlertDialog(
|
||||||
modifier = Modifier
|
onDismissRequest = { /* Do nothing to prevent dismissal by tapping outside */ },
|
||||||
.verticalScroll(scrollState)
|
title = {
|
||||||
.fillMaxSize(),
|
Text(text = "Settings")
|
||||||
) {
|
},
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
text = {
|
||||||
TopComponents()
|
AboutSettings(context = context,onCheckForUpdate)
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
},
|
||||||
OutlinedCard(
|
confirmButton = {},
|
||||||
modifier = Modifier
|
dismissButton = {
|
||||||
.padding(10.dp)
|
Button(onClick = onDismissDialogue) {
|
||||||
.wrapContentSize()
|
Text("Close")
|
||||||
) {
|
}
|
||||||
AboutSettings(context = context)
|
},
|
||||||
Developer_settings(context = context)
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components
|
package `in`.sminnovations.attendanceapp.screens.settings.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -19,9 +20,12 @@ import `in`.sminnovations.attendanceapp.R
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopComponents(
|
fun TopComponents(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.smi_logo),
|
painter = painterResource(id = R.drawable.smi_logo),
|
||||||
|
|||||||
@@ -66,6 +66,46 @@ fun ClickableCard(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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)
|
@OptIn( ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,55 +1,23 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
|
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import `in`.sminnovations.attendanceapp.R
|
import `in`.sminnovations.attendanceapp.R
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
|
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
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.CombinedClickableCard
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("CoroutineCreationDuringComposition")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutSettings(context: Context) {
|
fun AboutSettings(
|
||||||
val scope = rememberCoroutineScope()
|
context: Context ,
|
||||||
val dataStore = SettingsData(context)
|
onCheckUpdate : () -> Unit
|
||||||
var clickCount by remember { mutableIntStateOf(0) }
|
) {
|
||||||
if (clickCount > 5) {
|
Column() {
|
||||||
scope.launch {
|
|
||||||
dataStore.setDevMode(true)
|
|
||||||
}
|
|
||||||
clickCount = 0
|
|
||||||
Toast.makeText(context, "Developer mode is On : Scroll Down", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
Column(modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 10.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "About",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Start)
|
|
||||||
.padding(14.dp),
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
OutlinedCard {
|
OutlinedCard {
|
||||||
ClickableCard(
|
ClickableCard(
|
||||||
title = "Version",
|
title = "Version",
|
||||||
@@ -57,23 +25,27 @@ fun AboutSettings(context: Context) {
|
|||||||
context.packageName,
|
context.packageName,
|
||||||
0
|
0
|
||||||
).versionName,
|
).versionName,
|
||||||
onClick = {
|
onClick = {},
|
||||||
clickCount++
|
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)
|
icon = ImageVector.vectorResource(id = R.drawable.round_info_24)
|
||||||
|
|
||||||
)
|
)
|
||||||
CombinedClickableCard(title = "Developer",
|
CombinedClickableCard(title = "Developer",
|
||||||
subtitle = "SM Innovations Pvt Ltd",
|
subtitle = "ShanMukha Innovations Pvt Ltd",
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onDoubleClick = {
|
onDoubleClick = {
|
||||||
|
|
||||||
},
|
},
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.round_logo_dev_24)
|
icon = ImageVector.vectorResource(id = R.drawable.baseline_developer_mode_24)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
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.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import `in`.sminnovations.attendanceapp.R
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.SwitchCard
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Developer_settings(context: Context) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val dataStore = SettingsData(context)
|
|
||||||
val devMode = dataStore.getDevModeToggle.collectAsState(initial = false)
|
|
||||||
Column(modifier = Modifier.padding(10.dp,0.dp,10.dp,10.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.developer_options),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Start)
|
|
||||||
.padding(14.dp),
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
OutlinedCard {
|
|
||||||
SwitchCard(
|
|
||||||
checkedValue = devMode.value!!,
|
|
||||||
title = "Developer mode",
|
|
||||||
subtitle = "To toggle developer mode",
|
|
||||||
onSwitchChange = {
|
|
||||||
scope.launch {
|
|
||||||
dataStore.setDevMode(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
icon = ImageVector.vectorResource(R.drawable.round_developer_mode_24)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
ClickableCard(
|
|
||||||
title = "Clear the Database",
|
|
||||||
subtitle = "you will loose every thing 😨",
|
|
||||||
onClick = {
|
|
||||||
|
|
||||||
},
|
|
||||||
icon = ImageVector.vectorResource(R.drawable.round_delete_sweep_24)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,9 @@ import androidx.compose.ui.graphics.Color
|
|||||||
|
|
||||||
val Purple80 = Color(0xFFFF9800)
|
val Purple80 = Color(0xFFFF9800)
|
||||||
val PurpleGrey80 = Color(0xFFFFCB7E)
|
val PurpleGrey80 = Color(0xFFFFCB7E)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
val Pink80 = Color(0xFF9B6D7B)
|
||||||
|
|
||||||
val Purple40 = Color(0xFFB86E02)
|
val Purple40 = Color(0xFF000000)
|
||||||
val PurpleGrey40 = Color(0xFF865307)
|
val PurpleGrey40 = Color(0xFF865307)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package `in`.sminnovations.attendanceapp.main
|
package `in`.sminnovations.attendanceapp.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -13,6 +13,8 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -33,13 +35,16 @@ class NetworkMonitor @Inject constructor(
|
|||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
performPingCheck()
|
performPingCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
_isConnected.postValue(false)
|
_isConnected.postValue(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startMonitoring() {
|
fun startMonitoring() {
|
||||||
|
val network = connectivityManager.activeNetwork
|
||||||
|
if (network != null) {
|
||||||
|
performPingCheck()
|
||||||
|
}
|
||||||
val networkRequest = NetworkRequest.Builder()
|
val networkRequest = NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.build()
|
.build()
|
||||||
@@ -58,10 +63,13 @@ class NetworkMonitor @Inject constructor(
|
|||||||
private fun performPingCheck() {
|
private fun performPingCheck() {
|
||||||
networkScope.launch {
|
networkScope.launch {
|
||||||
try {
|
try {
|
||||||
// Ping Google's public DNS server
|
val url = URL("https://www.google.com")
|
||||||
val process = Runtime.getRuntime().exec("/system/bin/ping -c 1 8.8.8.8")
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
val isReachable = process.waitFor() == 0
|
connection.connectTimeout = 2000
|
||||||
|
connection.connect()
|
||||||
|
val isReachable = connection.responseCode == 200
|
||||||
_isConnected.postValue(isReachable)
|
_isConnected.postValue(isReachable)
|
||||||
|
connection.disconnect()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_isConnected.postValue(false)
|
_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_developer_mode_24.xml
Normal file
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>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
5
app/src/main/res/drawable/baseline_settings_24.xml
Normal file
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>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<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="M18.06,2.83c-1.15,-0.77 -2.46,-1.32 -3.86,-1.61C13.58,1.1 13,1.57 13,2.21v0c0,0.46 0.31,0.88 0.76,0.97c1.17,0.23 2.26,0.7 3.21,1.34c0.39,0.26 0.9,0.19 1.23,-0.14l0,0C18.66,3.93 18.59,3.18 18.06,2.83z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M11,2.21L11,2.21c0,-0.64 -0.58,-1.11 -1.2,-0.99c-1.4,0.29 -2.71,0.84 -3.86,1.61c-0.52,0.35 -0.59,1.1 -0.15,1.54l0,0c0.33,0.33 0.84,0.4 1.23,0.14c0.96,-0.64 2.04,-1.1 3.21,-1.34C10.69,3.09 11,2.67 11,2.21z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M4.38,5.79L4.38,5.79C3.93,5.34 3.18,5.42 2.84,5.94C2.07,7.09 1.51,8.39 1.23,9.8C1.1,10.42 1.58,11 2.21,11h0c0.46,0 0.88,-0.31 0.97,-0.76c0.23,-1.17 0.7,-2.26 1.34,-3.22C4.77,6.64 4.7,6.12 4.38,5.79z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M21.79,11L21.79,11c0.63,0 1.11,-0.58 0.98,-1.2c-0.29,-1.4 -0.84,-2.7 -1.61,-3.86c-0.35,-0.52 -1.1,-0.6 -1.54,-0.15l0,0c-0.33,0.33 -0.4,0.84 -0.14,1.23c0.64,0.96 1.1,2.05 1.34,3.22C20.91,10.69 21.33,11 21.79,11z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M8,12.46l2.44,1.11L11.54,16c0.18,0.39 0.73,0.39 0.91,0l1.11,-2.44L16,12.46c0.39,-0.18 0.39,-0.73 0,-0.91l-2.44,-1.11L12.46,8c-0.18,-0.39 -0.73,-0.39 -0.91,0l-1.11,2.44L8,11.54C7.61,11.72 7.61,12.28 8,12.46z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,21c-3.11,0 -5.85,-1.59 -7.46,-4H6c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H2c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-1.7c1.99,2.84 5.27,4.7 9,4.7c4.45,0 8.27,-2.64 10,-6.43c0.26,-0.57 -0.08,-1.25 -0.69,-1.39l0,0c-0.45,-0.1 -0.93,0.11 -1.12,0.54C18.77,18.83 15.64,21 12,21z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2S2,6.48 2,12S6.48,22 12,22zM13,4.07c3.94,0.49 7,3.85 7,7.93s-3.05,7.44 -7,7.93V4.07z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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,5h10v1c0,0.55 0.45,1 1,1s1,-0.45 1,-1L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,5zM16.12,15.88l3.17,-3.17c0.39,-0.39 0.39,-1.02 0,-1.41l-3.17,-3.17c-0.39,-0.39 -1.03,-0.39 -1.42,0 -0.39,0.39 -0.39,1.02 0,1.41L17.17,12l-2.47,2.47c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.03,0.39 1.42,0zM9.29,14.46L6.83,12l2.46,-2.46c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.03,-0.39 -1.42,0L4.7,11.3c-0.39,0.39 -0.39,1.02 0,1.41l3.17,3.17c0.39,0.39 1.03,0.39 1.42,0 0.4,-0.39 0.39,-1.03 0,-1.42zM17,19L7,19v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v1z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<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,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM7.68,14.98H6V9h1.71c1.28,0 1.71,1.03 1.71,1.71l0,2.56C9.42,13.95 9,14.98 7.68,14.98zM12.38,11.46v1.07h-1.18v1.39h1.93v1.07h-2.25c-0.4,0.01 -0.74,-0.31 -0.75,-0.71V9.75c-0.01,-0.4 0.31,-0.74 0.71,-0.75h2.28l0,1.07h-1.92v1.39H12.38zM16.88,14.23c-0.48,1.11 -1.33,0.89 -1.71,0L13.77,9h1.18l1.07,4.11L17.09,9h1.18L16.88,14.23z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M7.77,10.12H7.14v3.77h0.63c0.14,0 0.28,-0.05 0.42,-0.16c0.14,-0.1 0.21,-0.26 0.21,-0.47v-2.52c0,-0.21 -0.07,-0.37 -0.21,-0.47C8.05,10.17 7.91,10.12 7.77,10.12z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<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.65,15.67c0.14,-0.36 0.05,-0.77 -0.23,-1.05l-2.09,-2.06 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53h1.94c0.54,0 0.99,-0.45 0.99,-0.99v-0.02c0,-0.54 -0.45,-0.99 -0.99,-0.99L10,4L10,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v1L1.99,4c-0.54,0 -0.99,0.45 -0.99,0.99 0,0.55 0.45,0.99 0.99,0.99h10.18C11.5,7.92 10.44,9.75 9,11.35c-0.81,-0.89 -1.49,-1.86 -2.06,-2.88 -0.16,-0.29 -0.45,-0.47 -0.78,-0.47 -0.69,0 -1.13,0.75 -0.79,1.35 0.63,1.13 1.4,2.21 2.3,3.21L3.3,16.87c-0.4,0.39 -0.4,1.03 0,1.42 0.39,0.39 1.02,0.39 1.42,0L9,14l2.02,2.02c0.51,0.51 1.38,0.32 1.63,-0.35zM17.5,10c-0.6,0 -1.14,0.37 -1.35,0.94l-3.67,9.8c-0.24,0.61 0.22,1.26 0.87,1.26 0.39,0 0.74,-0.24 0.88,-0.61l0.89,-2.39h4.75l0.9,2.39c0.14,0.36 0.49,0.61 0.88,0.61 0.65,0 1.11,-0.65 0.88,-1.26l-3.67,-9.8c-0.22,-0.57 -0.76,-0.94 -1.36,-0.94zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
6
app/src/main/res/xml/provider_paths.xml
Normal file
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
|
package `in`.sminnovations.attendanceapp
|
||||||
|
|
||||||
|
import com.google.firebase.Timestamp
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import org.junit.Assert.*
|
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).
|
* 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).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
class ExampleUnitTest {
|
class ExampleUnitTest {
|
||||||
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun addition_isCorrect() {
|
fun addition_isCorrect() {
|
||||||
assertEquals(4, 2 + 2)
|
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())
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user