97 Commits

Author SHA1 Message Date
sathwikcs
44c1f77bc8 Merge remote-tracking branch 'origin/generic-dev-sacnner-app' into generic-dev-sacnner-app 2025-03-17 17:49:38 +05:30
sathwikcs
f00774af08 new version 3.1.5 , fixed the bug where check out is showing after check in 2025-03-17 17:49:13 +05:30
Sathwik C S
e7c52b9f43 Edit ExampleUnitTest.kt 2025-03-03 09:23:13 +00:00
Sathwik C S
b507137b82 Edit ExampleUnitTest.kt 2025-03-03 09:22:48 +00:00
sathwikcs
b395df6268 new version 3.1.1 2025-03-03 14:37:41 +05:30
sathwikcs
27058baf99 new version 3.1.1 2025-02-10 15:33:00 +05:30
sathwikcs
419d5b6cf1 Removed the 5 min dialogue 2025-02-07 14:59:49 +05:30
sathwikcs
e0d13f3bce Removed the 5 min dialogue 2025-02-07 14:59:44 +05:30
sathwikcs
2ad327cde8 Added the Auto Update Check Feature 2025-02-07 14:59:00 +05:30
sathwikcs
196a127110 Added the Auto Update Check Feature 2025-02-07 14:58:50 +05:30
sathwikcs
951bd2f8c6 Added the Auto Update Check Feature 2025-02-07 14:58:42 +05:30
sathwikcs
9925b9b8aa Added the Auto Update Check Feature 2025-02-07 14:58:37 +05:30
sathwikcs
48b336aade Added the Auto Update Check Feature 2025-02-07 14:58:31 +05:30
sathwikcs
0b8320ce29 UpdateDialogue.kt 2025-02-06 16:03:47 +05:30
sathwikcs
767bc0ea75 NetworkErrorDialog 2025-02-06 16:01:30 +05:30
sathwikcs
00359d3b8d AttendanceScreenContent.kt 2025-02-06 15:30:52 +05:30
sathwikcs
4f40559417 AttendanceScreenContent.kt 2025-02-06 15:30:34 +05:30
sathwikcs
2fefeee7ac home 2025-02-06 15:29:44 +05:30
sathwikcs
02b660d1ca homeVM 2025-02-06 15:29:17 +05:30
sathwikcs
5f969ac651 Network Error Dialogue 2025-02-06 15:25:59 +05:30
sathwikcs
057f71e292 UpdateDialog 2025-02-06 15:23:01 +05:30
sathwikcs
74300239a3 AppUpdator.kt 2025-02-06 15:22:37 +05:30
sathwikcs
5ade8d4d83 removed unit test 2025-02-05 18:22:06 +05:30
sathwikcs
d501d806e7 final 2025-02-05 18:19:44 +05:30
sathwikcs
abbbd28095 homVM 2025-02-05 18:06:06 +05:30
sathwikcs
8f9c912f41 homVM 2025-02-05 18:05:34 +05:30
sathwikcs
70aafe73be homVM 2025-02-05 18:05:14 +05:30
sathwikcs
5a5da9fe72 Attend 2025-02-05 18:04:42 +05:30
sathwikcs
c9fd5d7993 icons 2025-02-05 17:16:43 +05:30
sathwikcs
30adc892d4 homeVM 2025-02-05 17:15:52 +05:30
sathwikcs
ed001bd266 NetworkErrorDialog 2025-02-05 17:15:13 +05:30
sathwikcs
60cb946518 Settings.kt 2025-02-05 17:13:30 +05:30
sathwikcs
053579b685 home.kt 2025-02-05 17:13:09 +05:30
sathwikcs
0736a481c2 bye Connectivity.kt 2025-02-05 17:12:42 +05:30
sathwikcs
ec9ae99e03 AttendanceScreenContent 2025-02-05 17:12:07 +05:30
sathwikcs
3ebfd949eb about Card 2025-02-05 17:11:43 +05:30
sathwikcs
af34397f88 removed unnecessary resources 2025-02-05 15:54:11 +05:30
sathwikcs
b1a0b7b753 other 2025-02-04 12:01:30 +05:30
sathwikcs
95a2cd7e93 DeveloperCard.kt 2025-02-04 12:01:07 +05:30
sathwikcs
945f9920fd homeVM 2025-02-04 12:00:42 +05:30
sathwikcs
7adc89b4f3 home 2025-02-04 12:00:14 +05:30
sathwikcs
1883ef2316 Color.kt 2025-02-03 17:00:58 +05:30
sathwikcs
97c3ac172d data 2025-02-03 17:00:47 +05:30
sathwikcs
9f69174cc7 ExampleUnitTest 2025-02-03 17:00:37 +05:30
sathwikcs
d7cb04a782 Settings.kt 2025-02-03 17:00:11 +05:30
sathwikcs
273fbd50be UpdateDialog 2025-02-03 16:59:45 +05:30
sathwikcs
140a01c208 NetworkMonitor 2025-02-03 16:59:23 +05:30
sathwikcs
10eac95eec UpdateChecker 2025-02-03 16:58:58 +05:30
sathwikcs
183904838d AttendanceScreenContent 2025-02-03 16:58:34 +05:30
sathwikcs
fa75748e2c AppUpdator.kt 2025-02-03 16:57:59 +05:30
sathwikcs
cccbecf934 AppModule.kt 2025-02-03 16:57:10 +05:30
sathwikcs
b5012f12a9 AboutCard.kt 2025-02-03 16:56:40 +05:30
sathwikcs
972ca81b03 updated unit test for time difference 2025-01-31 12:05:32 +05:30
sathwikcs
53a339c8a6 updated unit test for time difference 2025-01-30 18:27:03 +05:30
sathwikcs
0eb3f8290d other 2025-01-30 18:19:20 +05:30
sathwikcs
e41adc5720 homeVM 2025-01-30 18:19:09 +05:30
sathwikcs
b49997889d homeV 2025-01-30 18:18:42 +05:30
sathwikcs
6e2092a2b0 unit test 2025-01-30 17:20:46 +05:30
sathwikcs
89577cf867 homeVm 2025-01-30 17:20:23 +05:30
sathwikcs
506ac3a9b9 Error Content 2025-01-30 17:19:39 +05:30
sathwikcs
c1f60baffb Merge remote-tracking branch 'origin/generic-dev-sacnner-app' into generic-dev-sacnner-app 2025-01-27 18:01:17 +05:30
sathwikcs
f41af9f130 MainScreen.kt 2025-01-27 18:01:03 +05:30
sathwikcs
fe823fea1d MainScreen.kt 2025-01-27 18:00:26 +05:30
sathwikcs
77e9f5d2ac home 2025-01-27 18:00:01 +05:30
sathwikcs
78b408e41a NavigationItem.kt 2025-01-27 17:59:27 +05:30
sathwikcs
b18c9e33c2 Bye bye Navigation.kt 2025-01-27 17:59:07 +05:30
sathwikcs
3a976d85d1 Bye bye NavAnimation.kt 2025-01-27 17:58:42 +05:30
sathwikcs
72a1cea7a5 Bye bye BottomNavBar.kt 2025-01-27 17:58:18 +05:30
sathwikcs
beabe015ca AppClass.kt 2025-01-27 17:57:53 +05:30
sathwikcs
5b507bbefd minor update 2025-01-27 17:48:09 +05:30
sathwikcs
4fc60e4180 minor update 2025-01-24 16:04:32 +05:30
sathwikcs
edb933146b other.xml 2025-01-23 11:57:54 +05:30
sathwikcs
9684559e70 Update 2025-01-23 11:57:14 +05:30
sathwikcs
3de809e23f scanner 2025-01-23 11:56:48 +05:30
sathwikcs
9275095d66 LoadingOverlay 2025-01-23 11:56:13 +05:30
sathwikcs
744d92221e build.gradle.kts 2025-01-23 11:55:54 +05:30
sathwikcs
161ac01820 AttendanceStatusCard.kt 2025-01-22 12:50:11 +05:30
sathwikcs
d4641e8f4d ScannerContent 2025-01-22 12:49:48 +05:30
sathwikcs
e32ff01664 homeVM 2025-01-22 12:49:08 +05:30
sathwikcs
fc47c86049 AttendanceScreenContent 2025-01-22 12:48:34 +05:30
sathwikcs
e6e4586ee9 Navigation 2025-01-17 15:21:53 +05:30
sathwikcs
32124886c5 Navigation 2025-01-17 15:21:36 +05:30
sathwikcs
fdf160b7a1 homeVM 2025-01-17 15:21:16 +05:30
sathwikcs
cf6b44b083 NetworkMonitor 2025-01-16 14:59:02 +05:30
sathwikcs
f7d540c6eb NetworkMonitor 2025-01-16 14:57:11 +05:30
sathwikcs
ff3a90aa9a MainScreen 2025-01-16 14:55:21 +05:30
sathwikcs
196d9f8eb5 DeveloperCard.kt 2025-01-16 14:54:32 +05:30
sathwikcs
65b95eda2f data 2025-01-16 14:54:01 +05:30
sathwikcs
7a22437bd5 AppUpdater 2025-01-16 14:53:31 +05:30
sathwikcs
1482c2b548 AppUpdater 2025-01-16 14:53:16 +05:30
sathwikcs
bdda880414 other.xml 2025-01-16 14:52:08 +05:30
sathwikcs
e137b0695f build.gradle.kts 2025-01-16 14:51:55 +05:30
sathwikcs
52882ac8a8 AndroidManifest.xml 2025-01-16 14:51:26 +05:30
sathwikcs
bc7cb275db provider_paths.xml 2025-01-16 14:51:00 +05:30
sathwikcs
ac10e6ac01 Qrscanner.kt 2025-01-16 14:50:41 +05:30
sathwikcs
58581101a5 TopComponents 2025-01-16 14:50:25 +05:30
sathwikcs
6054ba4ac1 UpdateDialog 2025-01-16 14:50:06 +05:30
46 changed files with 1359 additions and 695 deletions

View File

@@ -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
View File

@@ -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>

View File

@@ -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")
} }

View File

@@ -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>

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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()
} }
} }
) )

View File

@@ -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
}
})
}
}
}

View File

@@ -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
)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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")
)

View File

@@ -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,
)

View File

@@ -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() }
}

View File

@@ -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
)
}
}

View File

@@ -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)
)
}
}

View File

@@ -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 😊"
) )

View File

@@ -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)
)
}
}

View File

@@ -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)
)
}

View File

@@ -70,7 +70,7 @@ fun QRCodeScannerScreen(
AndroidView( AndroidView(
modifier = Modifier modifier = Modifier
.fillMaxHeight(.84f) .fillMaxHeight()
.fillMaxWidth() .fillMaxWidth()
, ,
factory = { previewView } factory = { previewView }

View File

@@ -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() }
}

View File

@@ -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
)
}
}
}

View File

@@ -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()
}

View File

@@ -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)
})
}
}
} }

View File

@@ -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
)

View File

@@ -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)
)
}
} }
} }

View File

@@ -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),

View File

@@ -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

View File

@@ -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)
) )
} }
} }
} }

View File

@@ -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)
)
}
}
}
}

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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)
} }

View File

@@ -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){
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="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>

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19.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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="updates"
path="." />
</paths>

View File

@@ -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())
}
*/
} }