Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6172074509
|
|||
|
0235b5d506
|
|||
|
7c18b56674
|
|||
|
cccdc2dd66
|
|||
|
62703cf2eb
|
|||
|
2c0ae23417
|
|||
|
87dcd08189
|
|||
|
f3dabbd3aa
|
|||
|
e4c6440758
|
|||
|
b478f05260
|
|||
|
afd954785a
|
|||
| d95c45abbb | |||
| 9df0b29ada | |||
|
ff9f0d6cc6
|
|||
|
61384623bd
|
|||
|
7da1893748
|
|||
|
f106244e57
|
|||
|
76a9120184
|
|||
|
abeed46c90
|
|||
|
7770997fd4
|
3
.gitignore
vendored
@@ -8,6 +8,7 @@ local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
@@ -32,4 +33,4 @@ render.experimental.xml
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
*.hprof
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/AndroidProjectSystem.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
860
.idea/caches/deviceStreaming.xml
generated
@@ -1,860 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceStreaming">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Sony" />
|
||||
<option name="codename" value="A402SO" />
|
||||
<option name="id" value="A402SO" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Sony" />
|
||||
<option name="name" value="Xperia 10" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2520" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP535DL1" />
|
||||
<option name="id" value="OP535DL1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2409" />
|
||||
<option name="screenDensity" value="401" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP5552L1" />
|
||||
<option name="id" value="OP5552L1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2415" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP5552L1" />
|
||||
<option name="id" value="OP5552L1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2415" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OPPO" />
|
||||
<option name="codename" value="OP573DL1" />
|
||||
<option name="id" value="OP573DL1" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="OPPO" />
|
||||
<option name="name" value="CPH2557" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a14m" />
|
||||
<option name="id" value="a14m" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-A145R" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2408" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a15" />
|
||||
<option name="id" value="a15" />
|
||||
<option name="labId" value="google" />
|
||||
<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="a15x" />
|
||||
<option name="id" value="a15x" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A15 5G" />
|
||||
<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="a16x" />
|
||||
<option name="id" value="a16x" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A16 5G" />
|
||||
<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="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A35" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="arcfox" />
|
||||
<option name="id" value="arcfox" />
|
||||
<option name="labId" value="google" />
|
||||
<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="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g 5G (2022)" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b6q" />
|
||||
<option name="id" value="b6q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Flip 6" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2640" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm2q" />
|
||||
<option name="id" value="dm2q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="S23 Plus" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="dubai" />
|
||||
<option name="id" value="dubai" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 30" />
|
||||
<option name="screenDensity" value="405" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e3q" />
|
||||
<option name="id" value="e3q" />
|
||||
<option name="labId" value="google" />
|
||||
<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="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Eos" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="eqe" />
|
||||
<option name="id" value="eqe" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 50 pro" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1220" />
|
||||
<option name="screenY" value="2712" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="fogona" />
|
||||
<option name="id" value="fogona" />
|
||||
<option name="labId" value="google" />
|
||||
<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="motorola" />
|
||||
<option name="codename" value="fogos" />
|
||||
<option name="id" value="fogos" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g34 5G" />
|
||||
<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="labId" value="google" />
|
||||
<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="labId" value="google" />
|
||||
<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="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts7lwifi" />
|
||||
<option name="id" value="gts7lwifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-T870" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<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="gts7xllite" />
|
||||
<option name="id" value="gts7xllite" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-T738U" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8wifi" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="gts8wifi" />
|
||||
<option name="labId" value="google" />
|
||||
<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="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="2304" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts9wifi" />
|
||||
<option name="id" value="gts9wifi" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-X710" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="lion" />
|
||||
<option name="id" value="lion" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g04" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1612" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="lyriq" />
|
||||
<option name="id" value="lyriq" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 40" />
|
||||
<option name="screenDensity" value="400" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="manaus" />
|
||||
<option name="id" value="manaus" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 40 neo" />
|
||||
<option name="screenDensity" value="400" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="maui" />
|
||||
<option name="id" value="maui" />
|
||||
<option name="labId" value="google" />
|
||||
<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="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21" />
|
||||
<option name="screenDensity" value="421" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="pa3q" />
|
||||
<option name="id" value="pa3q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S25 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<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="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="formFactor" value="Wear OS" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="r11q" />
|
||||
<option name="id" value="r11q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-S711U" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="t2q" />
|
||||
<option name="id" value="t2q" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S21 Plus" />
|
||||
<option name="screenDensity" value="394" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="formFactor" value="Tablet" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tegu" />
|
||||
<option name="id" value="tegu" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="36" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="default" value="true" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="xcover7" />
|
||||
<option name="id" value="xcover7" />
|
||||
<option name="labId" value="google" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-G556B" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2408" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
18
.idea/deploymentTargetSelector.xml
generated
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-09-07T04:49:14.182787Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/atridad/.android/avd/Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/gradle.xml
generated
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,50 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/kotlinc.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/migrations.xml
generated
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/misc.xml
generated
@@ -1,4 +0,0 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
17
.idea/runConfigurations.xml
generated
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
17
README.md
@@ -1,18 +1,27 @@
|
||||
# OpenClimb
|
||||
|
||||
This is a FOSS Android app meant to help climbers track their sessions, routes/problems, and overall progress. This app is offline-only and requires no special permissions to run. Its built using Jetpack Compose with Material You support.
|
||||
This is a FOSS app meant to help climbers track their sessions, routes/problems, and overall progress. This app is offline-only and requires no special permissions to run. Its built using Jetpack Compose with Material You support on Android and SwiftUI on iOS.
|
||||
|
||||
## Versions
|
||||
|
||||
- Android:1.4.2
|
||||
- iOS: 1.0.0
|
||||
|
||||
## Download
|
||||
|
||||
You have two options:
|
||||
For Android do one of the following:
|
||||
|
||||
1. Download the latest APK from the Releases page
|
||||
2. [<img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Obtainium" height="41">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.atridad.openclimb%22%2C%22url%22%3A%22https%3A%2F%2Fgit.atri.dad%2Fatridad%2FOpenClimb%2Freleases%22%2C%22author%22%3A%22git.atri.dad%22%2C%22name%22%3A%22OpenClimb%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22OpenClimb%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||
|
||||
For iOS:
|
||||
|
||||
**Stay tuned for an upcoming Testflight or App Store release!**
|
||||
|
||||
## Requirements
|
||||
|
||||
- Android 15+
|
||||
- Android 12+ or iOS 17+
|
||||
|
||||
## Contribution
|
||||
|
||||
As this is on my private git this will be difficult to do easily. Get in touch and I can figure something out.
|
||||
As this is on my private git this will be difficult to do easily. Get in touch and I can figure something out.
|
||||
|
||||
0
app/.gitignore → android/app/.gitignore
vendored
@@ -14,10 +14,10 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.atridad.openclimb"
|
||||
minSdk = 34
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 21
|
||||
versionName = "1.4.0"
|
||||
versionCode = 23
|
||||
versionName = "1.4.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -35,60 +35,48 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }
|
||||
|
||||
buildFeatures { compose = true }
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
|
||||
|
||||
dependencies {
|
||||
// Core Android libraries
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
|
||||
// Compose BOM and UI
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
|
||||
|
||||
// Room Database
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
|
||||
// Navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
|
||||
// ViewModel
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
|
||||
// Image Loading
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
@@ -103,4 +91,5 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ import com.atridad.openclimb.ui.theme.OpenClimbTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var shortcutAction by mutableStateOf<String?>(null)
|
||||
private var lastUsedGymId by mutableStateOf<String?>(null)
|
||||
|
||||
fun clearShortcutAction() {
|
||||
shortcutAction = null
|
||||
lastUsedGymId = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -21,11 +27,16 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
|
||||
shortcutAction = intent?.action
|
||||
lastUsedGymId = intent?.getStringExtra("LAST_USED_GYM_ID")
|
||||
|
||||
setContent {
|
||||
OpenClimbTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
OpenClimbApp(shortcutAction = shortcutAction)
|
||||
OpenClimbApp(
|
||||
shortcutAction = shortcutAction,
|
||||
lastUsedGymId = lastUsedGymId,
|
||||
onShortcutActionProcessed = { clearShortcutAction() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,5 +47,6 @@ class MainActivity : ComponentActivity() {
|
||||
setIntent(intent)
|
||||
|
||||
shortcutAction = intent.action
|
||||
lastUsedGymId = intent.getStringExtra("LAST_USED_GYM_ID")
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,26 @@
|
||||
package com.atridad.openclimb.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
||||
import com.atridad.openclimb.data.model.*
|
||||
import com.atridad.openclimb.utils.ZipExportImportUtils
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class ClimbRepository(
|
||||
database: OpenClimbDatabase,
|
||||
private val context: Context
|
||||
) {
|
||||
class ClimbRepository(database: OpenClimbDatabase, private val context: Context) {
|
||||
private val gymDao = database.gymDao()
|
||||
private val problemDao = database.problemDao()
|
||||
private val sessionDao = database.climbSessionDao()
|
||||
private val attemptDao = database.attemptDao()
|
||||
|
||||
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
|
||||
// Gym operations
|
||||
fun getAllGyms(): Flow<List<Gym>> = gymDao.getAllGyms()
|
||||
suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id)
|
||||
@@ -34,7 +28,7 @@ class ClimbRepository(
|
||||
suspend fun updateGym(gym: Gym) = gymDao.updateGym(gym)
|
||||
suspend fun deleteGym(gym: Gym) = gymDao.deleteGym(gym)
|
||||
fun searchGyms(query: String): Flow<List<Gym>> = gymDao.searchGyms(query)
|
||||
|
||||
|
||||
// Problem operations
|
||||
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
|
||||
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
|
||||
@@ -43,27 +37,36 @@ class ClimbRepository(
|
||||
suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem)
|
||||
suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem)
|
||||
fun searchProblems(query: String): Flow<List<Problem>> = problemDao.searchProblems(query)
|
||||
|
||||
|
||||
// Session operations
|
||||
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
||||
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId)
|
||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||
sessionDao.getSessionsByGym(gymId)
|
||||
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
||||
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
||||
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
|
||||
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
|
||||
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
|
||||
|
||||
suspend fun getLastUsedGym(): Gym? {
|
||||
val recentSessions = sessionDao.getRecentSessions(1).first()
|
||||
return if (recentSessions.isNotEmpty()) {
|
||||
getGymById(recentSessions.first().gymId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt operations
|
||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
|
||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
|
||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||
attemptDao.getAttemptsBySession(sessionId)
|
||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> =
|
||||
attemptDao.getAttemptsByProblem(problemId)
|
||||
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
|
||||
suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt)
|
||||
suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt)
|
||||
|
||||
|
||||
|
||||
// ZIP Export with images - Single format for reliability
|
||||
suspend fun exportAllDataToZip(directory: File? = null): File {
|
||||
try {
|
||||
@@ -72,47 +75,58 @@ class ClimbRepository(
|
||||
val allProblems = problemDao.getAllProblems().first()
|
||||
val allSessions = sessionDao.getAllSessions().first()
|
||||
val allAttempts = attemptDao.getAllAttempts().first()
|
||||
|
||||
|
||||
// Validate data integrity before export
|
||||
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||
|
||||
val exportData = ClimbDataExport(
|
||||
exportedAt = LocalDateTime.now().toString(),
|
||||
version = "1.0",
|
||||
gyms = allGyms,
|
||||
problems = allProblems,
|
||||
sessions = allSessions,
|
||||
attempts = allAttempts
|
||||
)
|
||||
|
||||
|
||||
val exportData =
|
||||
ClimbDataExport(
|
||||
exportedAt = LocalDateTime.now().toString(),
|
||||
version = "1.0",
|
||||
gyms = allGyms,
|
||||
problems = allProblems,
|
||||
sessions = allSessions,
|
||||
attempts = allAttempts
|
||||
)
|
||||
|
||||
// Collect all referenced image paths and validate they exist
|
||||
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||
val validImagePaths = referencedImagePaths.filter { imagePath ->
|
||||
try {
|
||||
val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath)
|
||||
imageFile.exists() && imageFile.length() > 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
val validImagePaths =
|
||||
referencedImagePaths
|
||||
.filter { imagePath ->
|
||||
try {
|
||||
val imageFile =
|
||||
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||
context,
|
||||
imagePath
|
||||
)
|
||||
imageFile.exists() && imageFile.length() > 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
|
||||
// Log any missing images for debugging
|
||||
val missingImages = referencedImagePaths - validImagePaths
|
||||
if (missingImages.isNotEmpty()) {
|
||||
android.util.Log.w("ClimbRepository", "Some referenced images are missing: $missingImages")
|
||||
android.util.Log.w(
|
||||
"ClimbRepository",
|
||||
"Some referenced images are missing: $missingImages"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return ZipExportImportUtils.createExportZip(
|
||||
context = context,
|
||||
exportData = exportData,
|
||||
referencedImagePaths = validImagePaths,
|
||||
directory = directory
|
||||
context = context,
|
||||
exportData = exportData,
|
||||
referencedImagePaths = validImagePaths,
|
||||
directory = directory
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Export failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
||||
try {
|
||||
// Collect all data with proper error handling
|
||||
@@ -120,72 +134,81 @@ class ClimbRepository(
|
||||
val allProblems = problemDao.getAllProblems().first()
|
||||
val allSessions = sessionDao.getAllSessions().first()
|
||||
val allAttempts = attemptDao.getAllAttempts().first()
|
||||
|
||||
|
||||
// Validate data integrity before export
|
||||
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||
|
||||
val exportData = ClimbDataExport(
|
||||
exportedAt = LocalDateTime.now().toString(),
|
||||
version = "1.0",
|
||||
gyms = allGyms,
|
||||
problems = allProblems,
|
||||
sessions = allSessions,
|
||||
attempts = allAttempts
|
||||
)
|
||||
|
||||
|
||||
val exportData =
|
||||
ClimbDataExport(
|
||||
exportedAt = LocalDateTime.now().toString(),
|
||||
version = "1.0",
|
||||
gyms = allGyms,
|
||||
problems = allProblems,
|
||||
sessions = allSessions,
|
||||
attempts = allAttempts
|
||||
)
|
||||
|
||||
// Collect all referenced image paths and validate they exist
|
||||
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||
val validImagePaths = referencedImagePaths.filter { imagePath ->
|
||||
try {
|
||||
val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath)
|
||||
imageFile.exists() && imageFile.length() > 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
val validImagePaths =
|
||||
referencedImagePaths
|
||||
.filter { imagePath ->
|
||||
try {
|
||||
val imageFile =
|
||||
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||
context,
|
||||
imagePath
|
||||
)
|
||||
imageFile.exists() && imageFile.length() > 0
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
|
||||
ZipExportImportUtils.createExportZipToUri(
|
||||
context = context,
|
||||
uri = uri,
|
||||
exportData = exportData,
|
||||
referencedImagePaths = validImagePaths
|
||||
context = context,
|
||||
uri = uri,
|
||||
exportData = exportData,
|
||||
referencedImagePaths = validImagePaths
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Export failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun importDataFromZip(file: File) {
|
||||
try {
|
||||
// Validate the ZIP file
|
||||
if (!file.exists() || file.length() == 0L) {
|
||||
throw Exception("Invalid ZIP file: file is empty or doesn't exist")
|
||||
}
|
||||
|
||||
|
||||
// Extract and validate the ZIP contents
|
||||
val importResult = ZipExportImportUtils.extractImportZip(context, file)
|
||||
|
||||
|
||||
// Validate JSON content
|
||||
if (importResult.jsonContent.isBlank()) {
|
||||
throw Exception("Invalid ZIP file: no data.json found or empty content")
|
||||
}
|
||||
|
||||
|
||||
// Parse and validate the data structure
|
||||
val importData = try {
|
||||
json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Invalid data format: ${e.message}")
|
||||
}
|
||||
|
||||
val importData =
|
||||
try {
|
||||
json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Invalid data format: ${e.message}")
|
||||
}
|
||||
|
||||
// Validate data integrity
|
||||
validateImportData(importData)
|
||||
|
||||
|
||||
// Clear existing data to avoid conflicts
|
||||
attemptDao.deleteAllAttempts()
|
||||
sessionDao.deleteAllSessions()
|
||||
problemDao.deleteAllProblems()
|
||||
gymDao.deleteAllGyms()
|
||||
|
||||
|
||||
// Import gyms first (problems depend on gyms)
|
||||
importData.gyms.forEach { gym ->
|
||||
try {
|
||||
@@ -194,13 +217,14 @@ class ClimbRepository(
|
||||
throw Exception("Failed to import gym ${gym.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Import problems with updated image paths
|
||||
val updatedProblems = ZipExportImportUtils.updateProblemImagePaths(
|
||||
importData.problems,
|
||||
importResult.importedImagePaths
|
||||
)
|
||||
|
||||
val updatedProblems =
|
||||
ZipExportImportUtils.updateProblemImagePaths(
|
||||
importData.problems,
|
||||
importResult.importedImagePaths
|
||||
)
|
||||
|
||||
updatedProblems.forEach { problem ->
|
||||
try {
|
||||
problemDao.insertProblem(problem)
|
||||
@@ -208,7 +232,7 @@ class ClimbRepository(
|
||||
throw Exception("Failed to import problem ${problem.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Import sessions
|
||||
importData.sessions.forEach { session ->
|
||||
try {
|
||||
@@ -217,7 +241,7 @@ class ClimbRepository(
|
||||
throw Exception("Failed to import session: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Import attempts last (depends on problems and sessions)
|
||||
importData.attempts.forEach { attempt ->
|
||||
try {
|
||||
@@ -226,61 +250,66 @@ class ClimbRepository(
|
||||
throw Exception("Failed to import attempt: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Import failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun validateDataIntegrity(
|
||||
gyms: List<Gym>,
|
||||
problems: List<Problem>,
|
||||
sessions: List<ClimbSession>,
|
||||
attempts: List<Attempt>
|
||||
gyms: List<Gym>,
|
||||
problems: List<Problem>,
|
||||
sessions: List<ClimbSession>,
|
||||
attempts: List<Attempt>
|
||||
) {
|
||||
// Validate that all problems reference valid gyms
|
||||
val gymIds = gyms.map { it.id }.toSet()
|
||||
val invalidProblems = problems.filter { it.gymId !in gymIds }
|
||||
if (invalidProblems.isNotEmpty()) {
|
||||
throw Exception("Data integrity error: ${invalidProblems.size} problems reference non-existent gyms")
|
||||
throw Exception(
|
||||
"Data integrity error: ${invalidProblems.size} problems reference non-existent gyms"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Validate that all sessions reference valid gyms
|
||||
val invalidSessions = sessions.filter { it.gymId !in gymIds }
|
||||
if (invalidSessions.isNotEmpty()) {
|
||||
throw Exception("Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms")
|
||||
throw Exception(
|
||||
"Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Validate that all attempts reference valid problems and sessions
|
||||
val problemIds = problems.map { it.id }.toSet()
|
||||
val sessionIds = sessions.map { it.id }.toSet()
|
||||
|
||||
val invalidAttempts = attempts.filter {
|
||||
it.problemId !in problemIds || it.sessionId !in sessionIds
|
||||
}
|
||||
|
||||
val invalidAttempts =
|
||||
attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds }
|
||||
if (invalidAttempts.isNotEmpty()) {
|
||||
throw Exception("Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions")
|
||||
throw Exception(
|
||||
"Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun validateImportData(importData: ClimbDataExport) {
|
||||
if (importData.gyms.isEmpty()) {
|
||||
throw Exception("Import data is invalid: no gyms found")
|
||||
}
|
||||
|
||||
|
||||
if (importData.version.isBlank()) {
|
||||
throw Exception("Import data is invalid: no version information")
|
||||
}
|
||||
|
||||
|
||||
// Check for reasonable data sizes to prevent malicious imports
|
||||
if (importData.gyms.size > 1000 ||
|
||||
importData.problems.size > 10000 ||
|
||||
importData.sessions.size > 10000 ||
|
||||
importData.attempts.size > 100000) {
|
||||
if (importData.gyms.size > 1000 ||
|
||||
importData.problems.size > 10000 ||
|
||||
importData.sessions.size > 10000 ||
|
||||
importData.attempts.size > 100000
|
||||
) {
|
||||
throw Exception("Import data is too large: possible corruption or malicious file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun resetAllData() {
|
||||
try {
|
||||
// Clear all data from database
|
||||
@@ -288,15 +317,14 @@ class ClimbRepository(
|
||||
sessionDao.deleteAllSessions()
|
||||
problemDao.deleteAllProblems()
|
||||
gymDao.deleteAllGyms()
|
||||
|
||||
|
||||
// Clear all images from storage
|
||||
clearAllImages()
|
||||
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Reset failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun clearAllImages() {
|
||||
try {
|
||||
// Get the images directory
|
||||
@@ -314,10 +342,10 @@ class ClimbRepository(
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class ClimbDataExport(
|
||||
val exportedAt: String,
|
||||
val version: String = "1.0",
|
||||
val gyms: List<Gym>,
|
||||
val problems: List<Problem>,
|
||||
val sessions: List<ClimbSession>,
|
||||
val attempts: List<Attempt>
|
||||
)
|
||||
val exportedAt: String,
|
||||
val version: String = "1.0",
|
||||
val gyms: List<Gym>,
|
||||
val problems: List<Problem>,
|
||||
val sessions: List<ClimbSession>,
|
||||
val attempts: List<Attempt>
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.atridad.openclimb.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
data class BottomNavigationItem(val screen: Screen, val icon: ImageVector, val label: String)
|
||||
|
||||
val bottomNavigationItems =
|
||||
listOf(
|
||||
BottomNavigationItem(
|
||||
screen = Screen.Sessions,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
label = "Sessions"
|
||||
),
|
||||
BottomNavigationItem(
|
||||
screen = Screen.Problems,
|
||||
icon = Icons.Default.Star,
|
||||
label = "Problems"
|
||||
),
|
||||
BottomNavigationItem(
|
||||
screen = Screen.Analytics,
|
||||
icon = Icons.Default.Info,
|
||||
label = "Analytics"
|
||||
),
|
||||
BottomNavigationItem(
|
||||
screen = Screen.Gyms,
|
||||
icon = Icons.Default.LocationOn,
|
||||
label = "Gyms"
|
||||
),
|
||||
BottomNavigationItem(
|
||||
screen = Screen.Settings,
|
||||
icon = Icons.Default.Settings,
|
||||
label = "Settings"
|
||||
)
|
||||
)
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -30,10 +31,16 @@ import com.atridad.openclimb.utils.NotificationPermissionUtils
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
fun OpenClimbApp(
|
||||
shortcutAction: String? = null,
|
||||
lastUsedGymId: String? = null,
|
||||
onShortcutActionProcessed: () -> Unit = {}
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val context = LocalContext.current
|
||||
|
||||
var lastUsedGym by remember { mutableStateOf<com.atridad.openclimb.data.model.Gym?>(null) }
|
||||
|
||||
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
||||
val repository = remember { ClimbRepository(database, context) }
|
||||
val viewModel: ClimbViewModel = viewModel(factory = ClimbViewModelFactory(repository))
|
||||
@@ -69,11 +76,19 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
val activeSession by viewModel.activeSession.collectAsState()
|
||||
val gyms by viewModel.gyms.collectAsState()
|
||||
|
||||
LaunchedEffect(activeSession, gyms) {
|
||||
// Update last used gym when gyms change
|
||||
LaunchedEffect(gyms) {
|
||||
if (gyms.isNotEmpty() && lastUsedGym == null) {
|
||||
lastUsedGym = viewModel.getLastUsedGym()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeSession, gyms, lastUsedGym) {
|
||||
AppShortcutManager.updateShortcuts(
|
||||
context = context,
|
||||
hasActiveSession = activeSession != null,
|
||||
hasGyms = gyms.isNotEmpty()
|
||||
hasGyms = gyms.isNotEmpty(),
|
||||
lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,22 +99,6 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
|
||||
if (activeSession == null && gyms.isNotEmpty()) {
|
||||
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
||||
!NotificationPermissionUtils.isNotificationPermissionGranted(
|
||||
context
|
||||
)
|
||||
) {
|
||||
showNotificationPermissionDialog = true
|
||||
} else {
|
||||
if (gyms.size == 1) {
|
||||
viewModel.startSession(context, gyms.first().id)
|
||||
} else {
|
||||
navController.navigate(Screen.AddEditSession())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppShortcutManager.ACTION_END_SESSION -> {
|
||||
navController.navigate(Screen.Sessions) {
|
||||
@@ -112,6 +111,62 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// Process shortcut actions after data is loaded
|
||||
LaunchedEffect(shortcutAction, activeSession, gyms, lastUsedGym) {
|
||||
if (shortcutAction == AppShortcutManager.ACTION_START_SESSION && gyms.isNotEmpty()) {
|
||||
android.util.Log.d(
|
||||
"OpenClimbApp",
|
||||
"Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}"
|
||||
)
|
||||
|
||||
if (activeSession == null) {
|
||||
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
||||
!NotificationPermissionUtils.isNotificationPermissionGranted(
|
||||
context
|
||||
)
|
||||
) {
|
||||
android.util.Log.d("OpenClimbApp", "Showing notification permission dialog")
|
||||
showNotificationPermissionDialog = true
|
||||
} else {
|
||||
if (gyms.size == 1) {
|
||||
android.util.Log.d(
|
||||
"OpenClimbApp",
|
||||
"Starting session with single gym: ${gyms.first().name}"
|
||||
)
|
||||
viewModel.startSession(context, gyms.first().id)
|
||||
} else {
|
||||
// Try to get the last used gym from the intent or fallback to state
|
||||
val targetGym =
|
||||
lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } }
|
||||
?: lastUsedGym
|
||||
|
||||
if (targetGym != null) {
|
||||
android.util.Log.d(
|
||||
"OpenClimbApp",
|
||||
"Starting session with target gym: ${targetGym.name}"
|
||||
)
|
||||
viewModel.startSession(context, targetGym.id)
|
||||
} else {
|
||||
android.util.Log.d(
|
||||
"OpenClimbApp",
|
||||
"No target gym found, navigating to selection"
|
||||
)
|
||||
navController.navigate(Screen.AddEditSession())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d(
|
||||
"OpenClimbApp",
|
||||
"Active session already exists: ${activeSession?.id}"
|
||||
)
|
||||
}
|
||||
|
||||
// Clear the shortcut action after processing to prevent repeated execution
|
||||
onShortcutActionProcessed()
|
||||
}
|
||||
}
|
||||
|
||||
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
||||
|
||||
Scaffold(
|
||||
@@ -155,6 +210,8 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
if (gyms.size == 1) {
|
||||
viewModel.startSession(context, gyms.first().id)
|
||||
} else {
|
||||
// Always show gym selection for FAB when
|
||||
// multiple gyms
|
||||
navController.navigate(Screen.AddEditSession())
|
||||
}
|
||||
}
|
||||
@@ -173,7 +230,6 @@ fun OpenClimbApp(shortcutAction: String? = null) {
|
||||
}
|
||||
|
||||
composable<Screen.Problems> {
|
||||
val gyms by viewModel.gyms.collectAsState()
|
||||
LaunchedEffect(gyms) {
|
||||
fabConfig =
|
||||
if (gyms.isNotEmpty()) {
|
||||
@@ -190,12 +190,18 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||
repository.getSessionsByGym(gymId)
|
||||
|
||||
// Get last used gym for shortcut functionality
|
||||
suspend fun getLastUsedGym(): Gym? = repository.getLastUsedGym()
|
||||
|
||||
// Active session management
|
||||
fun startSession(context: Context, gymId: String, notes: String? = null) {
|
||||
viewModelScope.launch {
|
||||
android.util.Log.d("ClimbViewModel", "startSession called with gymId: $gymId")
|
||||
|
||||
if (!com.atridad.openclimb.utils.NotificationPermissionUtils
|
||||
.isNotificationPermissionGranted(context)
|
||||
) {
|
||||
android.util.Log.d("ClimbViewModel", "Notification permission not granted")
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
error =
|
||||
@@ -206,6 +212,10 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
||||
|
||||
val existingActive = repository.getActiveSession()
|
||||
if (existingActive != null) {
|
||||
android.util.Log.d(
|
||||
"ClimbViewModel",
|
||||
"Active session already exists: ${existingActive.id}"
|
||||
)
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
error = "There's already an active session. Please end it first."
|
||||
@@ -213,15 +223,21 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() {
|
||||
return@launch
|
||||
}
|
||||
|
||||
android.util.Log.d("ClimbViewModel", "Creating new session")
|
||||
val newSession = ClimbSession.create(gymId = gymId, notes = notes)
|
||||
repository.insertSession(newSession)
|
||||
|
||||
android.util.Log.d(
|
||||
"ClimbViewModel",
|
||||
"Starting tracking service for session: ${newSession.id}"
|
||||
)
|
||||
// Start the tracking service
|
||||
val serviceIntent = SessionTrackingService.createStartIntent(context, newSession.id)
|
||||
context.startForegroundService(serviceIntent)
|
||||
|
||||
ClimbStatsWidgetProvider.updateAllWidgets(context)
|
||||
|
||||
android.util.Log.d("ClimbViewModel", "Session started successfully")
|
||||
_uiState.value = _uiState.value.copy(message = "Session started successfully!")
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,12 @@ object AppShortcutManager {
|
||||
const val ACTION_END_SESSION = "com.atridad.openclimb.action.END_SESSION"
|
||||
|
||||
/** Updates the app shortcuts based on current session state */
|
||||
fun updateShortcuts(context: Context, hasActiveSession: Boolean, hasGyms: Boolean) {
|
||||
fun updateShortcuts(
|
||||
context: Context,
|
||||
hasActiveSession: Boolean,
|
||||
hasGyms: Boolean,
|
||||
lastUsedGym: com.atridad.openclimb.data.model.Gym? = null
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
|
||||
|
||||
@@ -30,7 +35,7 @@ object AppShortcutManager {
|
||||
shortcuts.add(createEndSessionShortcut(context))
|
||||
} else if (hasGyms) {
|
||||
// Show "Start Session" shortcut when no active session but gyms exist
|
||||
shortcuts.add(createStartSessionShortcut(context))
|
||||
shortcuts.add(createStartSessionShortcut(context, lastUsedGym))
|
||||
}
|
||||
|
||||
shortcutManager.dynamicShortcuts = shortcuts
|
||||
@@ -38,16 +43,34 @@ object AppShortcutManager {
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
private fun createStartSessionShortcut(context: Context): ShortcutInfo {
|
||||
private fun createStartSessionShortcut(
|
||||
context: Context,
|
||||
lastUsedGym: com.atridad.openclimb.data.model.Gym? = null
|
||||
): ShortcutInfo {
|
||||
val startIntent =
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
action = ACTION_START_SESSION
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
lastUsedGym?.let { gym -> putExtra("LAST_USED_GYM_ID", gym.id) }
|
||||
}
|
||||
|
||||
val shortLabel =
|
||||
if (lastUsedGym != null) {
|
||||
"Start at ${lastUsedGym.name}"
|
||||
} else {
|
||||
context.getString(R.string.shortcut_start_session_short)
|
||||
}
|
||||
|
||||
val longLabel =
|
||||
if (lastUsedGym != null) {
|
||||
"Start a new climbing session at ${lastUsedGym.name}"
|
||||
} else {
|
||||
context.getString(R.string.shortcut_start_session_long)
|
||||
}
|
||||
|
||||
return ShortcutInfo.Builder(context, SHORTCUT_START_SESSION)
|
||||
.setShortLabel(context.getString(R.string.shortcut_start_session_short))
|
||||
.setLongLabel(context.getString(R.string.shortcut_start_session_long))
|
||||
.setShortLabel(shortLabel)
|
||||
.setLongLabel(longLabel)
|
||||
.setIcon(Icon.createWithResource(context, R.drawable.ic_play_arrow_24))
|
||||
.setIntent(startIntent)
|
||||
.build()
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |