diff --git a/.gitignore b/.gitignore
index f913413..834ecd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,16 @@
*.iml
.gradle
/local.properties
-/.idea/workspace.xml
+/.idea/caches
/.idea/libraries
-/.idea/sonarlint
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
+.cxx
+local.properties
+.kotlin/
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index b625119..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-ParkingLotDemo
\ No newline at end of file
diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
deleted file mode 100644
index 9021732..0000000
--- a/.idea/assetWizardSettings.xml
+++ /dev/null
@@ -1,321 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser
deleted file mode 100644
index 303262c..0000000
Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 663459a..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 099a47f..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..4bccd57
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..44ca2d9
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index 8e2e3ee..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index f8467b4..6d0ee1c 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 158d51d..0ad17cb 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,20 +1,6 @@
+
-
-
-
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 65e9598..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/other.xml b/.idea/other.xml
new file mode 100644
index 0000000..94c96f6
--- /dev/null
+++ b/.idea/other.xml
@@ -0,0 +1,318 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sonarlint/issuestore/0/6/063032c471ffd7d160be37cce333732f26f8d931 b/.idea/sonarlint/issuestore/0/6/063032c471ffd7d160be37cce333732f26f8d931
deleted file mode 100644
index e69de29..0000000
diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb
deleted file mode 100644
index 41aabc7..0000000
--- a/.idea/sonarlint/issuestore/index.pb
+++ /dev/null
@@ -1,3 +0,0 @@
-
-t
-Dapp/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt,0/6/063032c471ffd7d160be37cce333732f26f8d931
\ No newline at end of file
diff --git a/.idea/sonarlint/securityhotspotstore/0/6/063032c471ffd7d160be37cce333732f26f8d931 b/.idea/sonarlint/securityhotspotstore/0/6/063032c471ffd7d160be37cce333732f26f8d931
deleted file mode 100644
index e69de29..0000000
diff --git a/.idea/sonarlint/securityhotspotstore/index.pb b/.idea/sonarlint/securityhotspotstore/index.pb
deleted file mode 100644
index 41aabc7..0000000
--- a/.idea/sonarlint/securityhotspotstore/index.pb
+++ /dev/null
@@ -1,3 +0,0 @@
-
-t
-Dapp/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt,0/6/063032c471ffd7d160be37cce333732f26f8d931
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1dd..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index d0e577e..347326c 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,44 @@
-*Read this in other languages: [English](README.md), [中文](README.zh-tw.md).*
-
-# ParkingDemo
-Taipei City Parking Lot Information Query System Demo.
+# ParkingLot
+Example of parking lot query APP developed using Android.
### Screenshots
### Supported Android Versions
-- Android 5.1 Lollipop(API level 21) or higher.
+- Android Nougat 7.0(API level 24) or higher.
### Prepare
-Add your [GoogleMaps](https://developers.google.com/maps/documentation/android-api/) key to strings.xml and turn on API from [Google Cloud Platform](https://console.cloud.google.com/).
-
-### Data resource
-臺北市資料大平臺 - [臺北市停車場資訊](https://data.taipei/#/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c).
-
-### Used libraries
-1. [Gson](https://github.com/google/gson)
-2. [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle)
-3. [Maps](https://developers.google.com/maps/documentation/android-sdk/map?hl=zh-tw)
-4. [Maps-utils](https://github.com/googlemaps/android-maps-utils)
-5. [Material](https://material.io/)
-6. [OkHttp3](https://github.com/square/okhttp)
-7. [Retrofit2](https://github.com/square/retrofit)
-8. [Room](https://developer.android.com/topic/libraries/architecture/room)
-9. [RxJava2](https://github.com/ReactiveX/RxJava)
-10. [Sqlcipher](https://github.com/sqlcipher/android-database-sqlcipher)
-11. [ViewBinding](https://developer.android.com/topic/libraries/view-binding)
\ No newline at end of file
+Add your [GoogleMaps](https://developers.google.com/maps/documentation/android-api/) key to local.defaults.properties and turn on API from [Google Cloud Platform](https://console.cloud.google.com/).
+
+### Open Data resource
+Data Taipei - [臺北市停車場資訊](https://data.taipei/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c).
+
+### Libraries
+* [Android Jetpack](https://developer.android.com/jetpack)
+ * [Compose](https://developer.android.com/jetpack/androidx/releases/compose)
+ * [DataStore](https://developer.android.com/jetpack/androidx/releases/datastore)
+ * [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle)
+ * [Navigation](https://developer.android.com/jetpack/androidx/releases/navigation)
+ * [Paging3](https://developer.android.com/jetpack/androidx/releases/paging)
+ * [Room](https://developer.android.com/jetpack/androidx/releases/room)
+ * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)
+* [Android Maps Compose](https://github.com/googlemaps/android-maps-compose)
+* [Coroutines](https://developer.android.com/kotlin/coroutines)
+* [Dagger](https://github.com/google/dagger)
+* [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization)
+* [LeakCanary](https://square.github.io/leakcanary/getting_started/)
+* [Material](https://m2.material.io/develop/android)
+* [Retrofit2](https://square.github.io/retrofit/)
+* [Secrets Gradle](https://developers.google.com/maps/documentation/places/android-sdk/secrets-gradle-plugin?hl=zh-tw)
\ No newline at end of file
diff --git a/README.zh-tw.md b/README.zh-tw.md
deleted file mode 100644
index 2050b8e..0000000
--- a/README.zh-tw.md
+++ /dev/null
@@ -1,41 +0,0 @@
-*其他語言版本: [English](README.md), [中文](README.zh-tw.md).*
-
-# ParkingDemo
-台北市停車場查詢系統。
-
-
-
-### 畫面截圖
-
-
-
-
-### 支援Android版本
-- Android 5.1 Lollipop(API level 21)或更高。
-
-### 前置準備
-添加[GoogleMaps](https://developers.google.com/maps/documentation/android-api/) 金鑰至strings.xml並在[Google Cloud Platform](https://console.cloud.google.com/)啟用API。
-
-### 資料來源
-臺北市資料大平臺 - [臺北市停車場資訊](https://data.taipei/#/dataset/detail?id=d5c0656b-5250-4179-a491-c94daa56ef2c)。
-
-### 使用函示庫
-1. [Gson](https://github.com/google/gson)
-2. [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle)
-3. [Maps](https://developers.google.com/maps/documentation/android-sdk/map?hl=zh-tw)
-4. [Maps-utils](https://github.com/googlemaps/android-maps-utils)
-5. [Material](https://material.io/)
-6. [OkHttp3](https://github.com/square/okhttp)
-7. [Retrofit2](https://github.com/square/retrofit)
-8. [Room](https://developer.android.com/topic/libraries/architecture/room)
-9. [RxJava2](https://github.com/ReactiveX/RxJava)
-10. [Sqlcipher](https://github.com/sqlcipher/android-database-sqlcipher)
-11. [ViewBinding](https://developer.android.com/topic/libraries/view-binding)
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 1d737ce..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,104 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
- id 'com.google.devtools.ksp'
-}
-
-android {
- compileSdk 34
-
- defaultConfig {
- applicationId "com.a1573595.parkingdemo"
- minSdk 21
- targetSdk 34
- versionCode 5
- versionName "1.2.0"
- ndkVersion "23.1.7779620"
-
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
- 'proguard-gson.pro', 'proguard-rules.pro', 'proguard-sqlite.pro',
- 'proguard-okhttp.pro', 'proguard-retrofit2.pro'
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- ndk {
- debugSymbolLevel 'FULL'
- }
- }
-
- buildTypes {
- debug {
- debuggable true
- }
- release {
- debuggable false
- minifyEnabled true
- shrinkResources true
- }
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = '17'
- }
- buildFeatures {
- viewBinding true
- }
- testOptions.unitTests {
- includeAndroidResources = true
- }
- namespace 'com.a1573595.parkinglotdemo'
-}
-
-dependencies {
- def retrofit_version = "2.9.0"
- def okHttp_version = '4.12.0'
- def room_version = "2.6.1"
- def paging_version = "3.2.1"
-
- implementation 'androidx.core:core-ktx:1.12.0'
- implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.11.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.activity:activity-ktx:1.8.2'
-
- implementation "androidx.paging:paging-runtime-ktx:$paging_version"
- implementation "androidx.paging:paging-rxjava3:$paging_version"
-
- implementation 'com.google.android.gms:play-services-maps:18.2.0'
- implementation 'com.google.android.gms:play-services-location:21.2.0'
- implementation 'com.google.maps.android:android-maps-utils:3.8.2'
-
- implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
- implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
-
- implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
- implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version"
- implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_version"
-
- implementation 'com.google.code.gson:gson:2.10.1'
-
- implementation 'androidx.datastore:datastore-preferences-rxjava3:1.0.0'
-
- implementation "androidx.room:room-runtime:$room_version"
- ksp "androidx.room:room-compiler:$room_version"
- implementation "androidx.room:room-rxjava3:$room_version"
- implementation 'net.zetetic:android-database-sqlcipher:4.5.4'
- implementation "androidx.sqlite:sqlite-ktx:2.4.0"
-
- implementation 'com.jakewharton.timber:timber:5.0.1'
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
-
- testImplementation 'junit:junit:4.13.2'
- testImplementation 'androidx.arch.core:core-testing:2.2.0'
- testImplementation 'androidx.test.ext:junit-ktx:1.1.5'
- testImplementation 'androidx.test:core-ktx:1.5.0'
- testImplementation 'org.robolectric:robolectric:4.11.1'
- testImplementation 'io.mockk:mockk:1.13.10'
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..0293444
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,111 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.google.dagger.hilt.android)
+ alias(libs.plugins.jetbrains.android)
+ alias(libs.plugins.jetbrains.compose.compiler)
+ alias(libs.plugins.jetbrains.kotlin.serialization)
+ id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
+ id("dagger.hilt.android.plugin")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "com.a1573595.parkingdemo"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.a1573595.parkingdemo"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 5
+ versionName = "1.2.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro", "proguard-retrofit2.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ buildConfig = true
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material.icons.extended)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.maps.compose)
+ implementation(libs.maps.compose.utils)
+
+ // Dagger Hilt
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+ implementation(libs.androidx.hilt.navigation.compose)
+
+ // DataStore
+ implementation(libs.androidx.datastore.preferences)
+
+ // Paging3
+ implementation(libs.androidx.paging.runtime.ktx)
+ implementation(libs.androidx.paging.compose)
+
+ // Retrofit
+ implementation(libs.retrofit)
+ implementation(libs.logging.interceptor)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.retrofit2.kotlinx.serialization.converter)
+
+ // Room
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.room.compiler)
+ implementation(libs.androidx.room.ktx)
+ implementation(libs.androidx.room.paging)
+
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+ debugImplementation(libs.leakcanary.android)
+
+ testImplementation(libs.junit)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+}
+
+secrets {
+ defaultPropertiesFileName = "local.defaults.properties"
+}
\ No newline at end of file
diff --git a/app/proguard-gson.pro b/app/proguard-gson.pro
deleted file mode 100644
index 2ce8b74..0000000
--- a/app/proguard-gson.pro
+++ /dev/null
@@ -1,25 +0,0 @@
-# Gson uses generic type information stored in a class file when working with fields. Proguard
-# removes such information by default, so configure it to keep all of it.
--keepattributes Signature
-
-# For using GSON @Expose annotation
--keepattributes *Annotation*
-
-# Gson specific classes
--dontwarn sun.misc.**
-#-keep class com.google.gson.stream.** { *; }
-
-# Application classes that will be serialized/deserialized over Gson
--keepclassmembers class com.alfred.model.** { *; }
-
-# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
-# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
--keep class * extends com.google.gson.TypeAdapter
--keep class * implements com.google.gson.TypeAdapterFactory
--keep class * implements com.google.gson.JsonSerializer
--keep class * implements com.google.gson.JsonDeserializer
-
-# Prevent R8 from leaving Data object members always null
--keepclassmembers,allowobfuscation class * {
- @com.google.gson.annotations.SerializedName ;
-}
\ No newline at end of file
diff --git a/app/proguard-sqlite.pro b/app/proguard-sqlite.pro
deleted file mode 100644
index 12b772f..0000000
--- a/app/proguard-sqlite.pro
+++ /dev/null
@@ -1,2 +0,0 @@
--keep,includedescriptorclasses class net.sqlcipher.** { *; }
--keep,includedescriptorclasses interface net.sqlcipher.** { *; }
\ No newline at end of file
diff --git a/app/proguard-square-okhttp.pro b/app/proguard-square-okhttp.pro
deleted file mode 100644
index 70825b7..0000000
--- a/app/proguard-square-okhttp.pro
+++ /dev/null
@@ -1,11 +0,0 @@
-# JSR 305 annotations are for embedding nullability information.
--dontwarn javax.annotation.**
-
-# A resource is loaded with a relative path so the package of this class must be preserved.
--keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-
-# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
--dontwarn org.codehaus.mojo.animal_sniffer.*
-
-# OkHttp platform used only on JVM and when Conscrypt dependency is available.
--dontwarn okhttp3.internal.platform.ConscryptPlatform
\ No newline at end of file
diff --git a/app/proguard-square-retrofit2.pro b/app/proguard-square-retrofit2.pro
index 0d2b0fc..6c1daaf 100644
--- a/app/proguard-square-retrofit2.pro
+++ b/app/proguard-square-retrofit2.pro
@@ -5,6 +5,9 @@
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
+# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
+-keepattributes AnnotationDefault
+
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* ;
@@ -26,4 +29,20 @@
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* ; }
--keep,allowobfuscation interface <1>
\ No newline at end of file
+-keep,allowobfuscation interface <1>
+
+# Keep inherited services.
+-if interface * { @retrofit2.http.* ; }
+-keep,allowobfuscation interface * extends <1>
+
+# With R8 full mode generic signatures are stripped for classes that are not
+# kept. Suspend functions are wrapped in continuations where the type argument
+# is used.
+-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
+
+# R8 full mode strips generic signatures from return types if not kept.
+-if interface * { @retrofit2.http.* public *** *(...); }
+-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
+
+# With R8 full mode generic signatures are stripped for classes that are not kept.
+-keep,allowobfuscation,allowshrinking class retrofit2.Response
\ No newline at end of file
diff --git a/app/release/app-release.aab b/app/release/app-release.aab
deleted file mode 100644
index 60b53fc..0000000
Binary files a/app/release/app-release.aab and /dev/null differ
diff --git a/app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt
similarity index 83%
rename from app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt
rename to app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt
index 679447a..0d26a65 100644
--- a/app/src/androidTest/java/com/a1573595/parkinglotdemo/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/a1573595/parkingdemo/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.a1573595.parkinglotdemo
+package com.a1573595.parkingdemo
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.a1573595.parkinglotdemo", appContext.packageName)
+ assertEquals("com.a1573595.parkinglot", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d685349..053008f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,49 +1,36 @@
-
+
+ android:theme="@style/Theme.ParkingLot"
+ tools:targetApi="31">
+ android:value="${MAPS_API_KEY}" />
+ android:theme="@style/Theme.ParkingLot">
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt b/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt
new file mode 100644
index 0000000..597f015
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ParkingLotApplication.kt
@@ -0,0 +1,7 @@
+package com.a1573595.parkingdemo
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class ParkingLotApplication : Application()
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt b/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt
new file mode 100644
index 0000000..a184b6c
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/AsyncValue.kt
@@ -0,0 +1,30 @@
+package com.a1573595.parkingdemo.common
+
+sealed class AsyncValue {
+ data object Loading : AsyncValue()
+
+ data class Error(val throwable: Throwable) : AsyncValue()
+
+ data class Data(val data: T) : AsyncValue()
+
+ val isLoading: Boolean
+ get() = this is Loading
+
+ val isError: Boolean
+ get() = this is Error
+
+ val isSuccess: Boolean
+ get() = this is Data
+
+ val error: Throwable?
+ get() = (this as? Error)?.throwable
+
+ val requireError: Throwable
+ get() = (this as Error).throwable
+
+ val value: T?
+ get() = (this as? Data)?.data
+
+ val requireValue: T
+ get() = (this as Data).data
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt
new file mode 100644
index 0000000..4996e1c
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/Base64EncodeDecode.kt
@@ -0,0 +1,29 @@
+package com.a1573595.parkingdemo.common
+
+import android.os.Build
+import android.util.Base64 as AndroidBase64
+import java.util.Base64 as JavaBase64
+
+object Base64EncodeDecode {
+ fun String.encodeToBase64(): String =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ JavaBase64.getUrlEncoder().encodeToString(this.toByteArray())
+ } else {
+ AndroidBase64.encodeToString(
+ this.toByteArray(),
+ AndroidBase64.URL_SAFE or AndroidBase64.NO_PADDING
+ )
+ }
+
+ fun String.decodeFromBase64(): String =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ String(JavaBase64.getUrlDecoder().decode(this))
+ } else {
+ String(
+ AndroidBase64.decode(
+ this,
+ AndroidBase64.URL_SAFE or AndroidBase64.NO_PADDING
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt
new file mode 100644
index 0000000..c7f192f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/Constants.kt
@@ -0,0 +1,5 @@
+package com.a1573595.parkingdemo.common
+
+object Constants {
+ const val BASE_URL = "https://tcgbusfs.blob.core.windows.net/blobtcmsv/"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt
new file mode 100644
index 0000000..9ce682c
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/Extension.kt
@@ -0,0 +1,15 @@
+package com.a1573595.parkingdemo.common
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.transform
+
+fun Double?.isNullOrEmpty(): Boolean = this == null || this == 0.0
+
+fun Flow.throttleLatest(delayMillis: Long): Flow = this
+ .conflate()
+ .transform {
+ emit(it)
+ delay(delayMillis)
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt
new file mode 100644
index 0000000..ceb1733
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/Format.kt
@@ -0,0 +1,7 @@
+package com.a1573595.parkingdemo.common
+
+import android.annotation.SuppressLint
+import java.text.SimpleDateFormat
+
+@SuppressLint("SimpleDateFormat")
+val dateFormatIso8601 = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt b/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt
new file mode 100644
index 0000000..7c2c543
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/LatLngConverter.kt
@@ -0,0 +1,63 @@
+package com.a1573595.parkingdemo.common
+
+import kotlin.math.cos
+import kotlin.math.pow
+import kotlin.math.sin
+import kotlin.math.tan
+
+class LatLngConverter {
+ companion object {
+ private const val a = 6378137.0
+ private const val b = 6356752.314245
+ private const val lon0 = 121 * Math.PI / 180
+ private const val k0 = 0.9999
+ private const val dx = 250000
+
+ fun twd97ToLonLat(x: Double, y: Double): Pair {
+ var x = x
+ var y = y
+ val dy = 0.0
+ val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5)
+ x -= dx.toDouble()
+ y -= dy
+
+ // Calculate the Meridional Arc
+ val M = y / k0
+
+ // Calculate Footprint Latitude
+ val mu = M / (a * (1.0 - e.pow(2.0) / 4.0 - 3 * e.pow(4.0) / 64.0 - 5 * e.pow(6.0) / 256.0))
+ val e1 = (1.0 - (1.0 - e.pow(2.0)).pow(0.5)) / (1.0 + (1.0 - e.pow(2.0)).pow(0.5))
+ val j1 = 3 * e1 / 2 - 27 * e1.pow(3.0) / 32.0
+ val j2 = 21 * e1.pow(2.0) / 16 - 55 * e1.pow(4.0) / 32.0
+ val j3 = 151 * e1.pow(3.0) / 96.0
+ val j4 = 1097 * e1.pow(4.0) / 512.0
+ val fp = mu + j1 * sin(2 * mu) + j2 * sin(4 * mu) + j3 * sin(6 * mu) + j4 * sin(8 * mu)
+
+ // Calculate Latitude and Longitude
+ val e2 = (e * a / b).pow(2.0)
+ val c1 = (e2 * cos(fp)).pow(2.0)
+ val t1 = tan(fp).pow(2.0)
+ val r1 = a * (1 - e.pow(2.0)) / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(3.0 / 2.0)
+ val n1 = a / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(0.5)
+ val D = x / (n1 * k0)
+
+ // Calculate latitude
+ val q1 = n1 * tan(fp) / r1
+ val q2 = D.pow(2.0) / 2.0
+ val q3 = (5 + 3 * t1 + 10 * c1 - 4 * c1.pow(2.0) - 9 * e2) * D.pow(4.0) / 24.0
+ val q4 = (61 + 90 * t1 + 298 * c1 + 45 * t1.pow(2.0) - 3 * c1.pow(2.0) - 252 * e2) * D.pow(6.0) / 720.0
+ var lat = fp - q1 * (q2 - q3 + q4)
+
+ // Calculate longitude
+ val q6 = (1 + 2 * t1 + c1) * D.pow(3.0) / 6
+ val q7 = (5 - 2 * c1 + 28 * t1 - 3 * c1.pow(2.0) + 8 * e2 + 24 * t1.pow(2.0)) * D.pow(5.0) / 120.0
+ var lon = lon0 + (D - q6 + q7) / cos(fp)
+
+ // Convert to degrees
+ lat = lat * 180 / Math.PI
+ lon = lon * 180 / Math.PI
+
+ return Pair(lat, lon)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt b/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt
new file mode 100644
index 0000000..fab032f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/common/Serializable.kt
@@ -0,0 +1,9 @@
+package com.a1573595.parkingdemo.common
+
+import kotlinx.serialization.json.Json
+
+val jsonConvert = Json {
+ allowStructuredMapKeys = true
+ isLenient = true
+ ignoreUnknownKeys = true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt
new file mode 100644
index 0000000..f42110d
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/Favorite.kt
@@ -0,0 +1,21 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+
+@Entity(
+ foreignKeys = [
+ ForeignKey(
+ entity = ParkingLot::class,
+ parentColumns = ["id"],
+ childColumns = ["id"],
+ onDelete = ForeignKey.CASCADE,
+ ),
+ ],
+)
+data class Favorite(
+ @PrimaryKey
+ val id: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt
new file mode 100644
index 0000000..512aa10
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/FavoriteDao.kt
@@ -0,0 +1,23 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Query
+import androidx.room.Upsert
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface FavoriteDao {
+ @Query("SELECT * FROM Favorite INNER JOIN ParkingLot ON Favorite.id = ParkingLot.id Order By Favorite.rowid DESC")
+ fun getParkingLotListFlow(): Flow>
+
+ @Query("SELECT * FROM Favorite WHERE id LIKE :id")
+ fun getFavoriteByIdFlow(id: String): Flow
+
+ @Upsert
+ suspend fun upsert(favorite: Favorite)
+
+ @Delete
+ suspend fun delete(favorite: Favorite)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt
new file mode 100644
index 0000000..cc11eaa
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/History.kt
@@ -0,0 +1,21 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+
+@Entity(
+ foreignKeys = [
+ ForeignKey(
+ entity = ParkingLot::class,
+ parentColumns = ["id"],
+ childColumns = ["id"],
+ onDelete = ForeignKey.CASCADE,
+ ),
+ ],
+)
+data class History(
+ @PrimaryKey
+ val id: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt
new file mode 100644
index 0000000..adfd6c3
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/HistoryDao.kt
@@ -0,0 +1,27 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Upsert
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface HistoryDao {
+ @Query("SELECT * FROM History INNER JOIN ParkingLot ON History.id = ParkingLot.id Order By History.rowid DESC")
+ fun getParkingLotListFlow(): Flow>
+
+ @Upsert
+ suspend fun upsert(history: History)
+
+ @Delete
+ suspend fun delete(history: History)
+
+ @Transaction
+ suspend fun deleteAndUpsert(history: History) {
+ delete(history)
+ upsert(history)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt
new file mode 100644
index 0000000..968eeb9
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDao.kt
@@ -0,0 +1,30 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.paging.PagingSource
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.RawQuery
+import androidx.room.Transaction
+import androidx.room.Upsert
+import androidx.sqlite.db.SupportSQLiteQuery
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface ParkingLotDao {
+ @Query("SELECT * FROM ParkingLot Order By id")
+ fun getParkingLotListFlow(): Flow>
+
+ @Query("SELECT * FROM ParkingLot WHERE id=:id")
+ suspend fun getParkingLotById(id: String): ParkingLot?
+
+ @Query("SELECT * FROM ParkingLot WHERE name LIKE '%' || :keyword || '%' OR address LIKE '%' || :keyword || '%' Order By id")
+ fun pagingSource(keyword: String): PagingSource
+
+ @RawQuery(observedEntities = [ParkingLot::class])
+ fun pagingSource(query: SupportSQLiteQuery): PagingSource
+
+ @Transaction
+ @Upsert
+ suspend fun upsertAll(parkingLotList: List): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt
new file mode 100644
index 0000000..d5fd929
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/local/ParkingLotDatabase.kt
@@ -0,0 +1,22 @@
+package com.a1573595.parkingdemo.data.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+
+@Database(
+ version = 1,
+ entities = [
+ ParkingLot::class,
+ Favorite::class,
+ History::class,
+ ],
+// exportSchema = false,
+)
+abstract class ParkingLotDatabase : RoomDatabase() {
+ abstract val parkingLotDao: ParkingLotDao
+
+ abstract val favoriteDao: FavoriteDao
+
+ abstract val historyDao: HistoryDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt b/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt
new file mode 100644
index 0000000..ae6bbf4
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/model/ParkingLotDataSet.kt
@@ -0,0 +1,36 @@
+package com.a1573595.parkingdemo.data.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ParkingLotDataSet(
+ val data: Data,
+)
+
+@Serializable
+data class Data(
+ val park: List,
+)
+
+@Serializable
+data class Park(
+ val id: String?,
+ val area: String?,
+ val name: String?,
+ val summary: String?,
+ val address: String?,
+ val tel: String?,
+ @SerialName("payex")
+ val payEx: String?,
+ val tw97x: Double?,
+ val tw97y: Double?,
+ @SerialName("totalcar")
+ val totalCar: Int?,
+ @SerialName("totalmotor")
+ val totalMotor: Int?,
+ @SerialName("totalbike")
+ val totalBike: Int?,
+ @SerialName("totalbus")
+ val totalBus: Int?,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt b/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt
new file mode 100644
index 0000000..6c18006
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/network/ParkingLotApi.kt
@@ -0,0 +1,10 @@
+package com.a1573595.parkingdemo.data.network
+
+import okhttp3.ResponseBody
+import retrofit2.http.GET
+
+fun interface ParkingLotApi {
+ @GET("TCMSV_alldesc.gz")
+// suspend fun getParkingLotDataSet(): Response
+ suspend fun getParkingLotDataSet(): ResponseBody
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt b/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt
new file mode 100644
index 0000000..17a249e
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/data/repository/ParkingLotRepositoryImpl.kt
@@ -0,0 +1,119 @@
+package com.a1573595.parkingdemo.data.repository
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.sqlite.db.SimpleSQLiteQuery
+import com.a1573595.parkingdemo.common.isNullOrEmpty
+import com.a1573595.parkingdemo.common.jsonConvert
+import com.a1573595.parkingdemo.data.local.Favorite
+import com.a1573595.parkingdemo.data.local.FavoriteDao
+import com.a1573595.parkingdemo.data.local.History
+import com.a1573595.parkingdemo.data.local.HistoryDao
+import com.a1573595.parkingdemo.data.local.ParkingLotDao
+import com.a1573595.parkingdemo.data.model.ParkingLotDataSet
+import com.a1573595.parkingdemo.data.network.ParkingLotApi
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import java.io.ByteArrayOutputStream
+import java.util.zip.GZIPInputStream
+import javax.inject.Inject
+
+class ParkingLotRepositoryImpl @Inject constructor(
+ private val dataStore: DataStore,
+ private val parkingLotDao: ParkingLotDao,
+ private val favoriteDao: FavoriteDao,
+ private val historyDao: HistoryDao,
+ private val parkingLotApi: ParkingLotApi,
+) : ParkingLotRepository {
+ private companion object {
+ val KEY_LAST_UPDATE_TIME = longPreferencesKey(name = "lastUpdateTime")
+ }
+
+ override val lastUpdateTimeFlow: Flow
+ get() = dataStore.data.map {
+ it[KEY_LAST_UPDATE_TIME]
+ }
+
+ override val parkingLotListFlow: Flow>
+ get() = parkingLotDao.getParkingLotListFlow()
+
+ override val favoriteParkingLotListFlow: Flow>
+ get() = favoriteDao.getParkingLotListFlow()
+
+ override val historyParkingLotListFlow: Flow>
+ get() = historyDao.getParkingLotListFlow()
+
+ override suspend fun fetchParkingLotDataSet(): Unit {
+ val respond = parkingLotApi.getParkingLotDataSet()
+ val inputStream = GZIPInputStream(respond.byteStream())
+
+ val buffer = ByteArray(256)
+ val outputStream = ByteArrayOutputStream()
+
+ var length: Int
+ while (inputStream.read(buffer).also { length = it } >= 0) {
+ outputStream.write(buffer, 0, length)
+ }
+
+ val dataSet = jsonConvert
+ .decodeFromString(outputStream.toString("UTF-8"))
+
+ val parkingLotList =
+ dataSet.data.park.filterNot { it.id.isNullOrEmpty() && it.tw97x.isNullOrEmpty() && it.tw97y.isNullOrEmpty() }
+ .map { ParkingLot.fromPark(it) }
+
+ parkingLotDao.upsertAll(parkingLotList)
+ dataStore.edit {
+ it[KEY_LAST_UPDATE_TIME] = System.currentTimeMillis()
+ }
+ }
+
+ override fun searchParkingLotPagingDataFlow(
+ keyword: String,
+ hasBus: Boolean,
+ hasCar: Boolean,
+ hasMotor: Boolean,
+ hasBike: Boolean,
+ ): Flow> {
+ val builder = StringBuilder()
+ builder.append("SELECT * FROM ParkingLot")
+ builder.append(" WHERE (name LIKE '%%$keyword%%' OR address LIKE '%%$keyword%%')")
+
+ if (hasBus) {
+ builder.append(" AND totalBus > 0")
+ }
+ if (hasCar) {
+ builder.append(" AND totalCar > 0")
+ }
+ if (hasMotor) {
+ builder.append(" AND totalMotor > 0")
+ }
+ if (hasBike) {
+ builder.append(" AND totalBike > 0")
+ }
+
+ return Pager(
+ config = PagingConfig(pageSize = 30, prefetchDistance = 2),
+ pagingSourceFactory = { parkingLotDao.pagingSource(SimpleSQLiteQuery(builder.toString())) },
+ ).flow
+ }
+
+ override suspend fun getParkingLotById(id: String): ParkingLot? = parkingLotDao.getParkingLotById(id)?.apply {
+ historyDao.deleteAndUpsert(History(id))
+ }
+
+ override fun getFavoriteByIdFlow(id: String): Flow = favoriteDao.getFavoriteByIdFlow(id)
+
+ override suspend fun upsertFavoriteById(id: String): Unit = favoriteDao.upsert(Favorite(id))
+
+ override suspend fun deleteFavoriteById(id: String): Unit = favoriteDao.delete(Favorite(id))
+
+ override suspend fun deleteHistoryById(id: String): Unit = historyDao.delete(History(id))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt
new file mode 100644
index 0000000..10005c6
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/di/LocalModule.kt
@@ -0,0 +1,64 @@
+package com.a1573595.parkingdemo.di
+
+import android.app.Application
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.room.Room
+import com.a1573595.parkingdemo.data.local.FavoriteDao
+import com.a1573595.parkingdemo.data.local.HistoryDao
+import com.a1573595.parkingdemo.data.local.ParkingLotDao
+import com.a1573595.parkingdemo.data.local.ParkingLotDatabase
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object LocalModule {
+ @Provides
+ @Singleton
+ fun provideParkingLotDataStore(@ApplicationContext appContext: Context): DataStore =
+ PreferenceDataStoreFactory.create(
+ produceFile = {
+ appContext.preferencesDataStoreFile("parkingLot_preferences")
+ }
+ )
+
+ @Provides
+ @Singleton
+ fun provideParkingLotDatabase(
+ application: Application
+ ): ParkingLotDatabase {
+ return Room.databaseBuilder(
+ context = application,
+ klass = ParkingLotDatabase::class.java,
+ name = "news_db"
+ )
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideParkingLotDao(
+ database: ParkingLotDatabase
+ ): ParkingLotDao = database.parkingLotDao
+
+ @Provides
+ @Singleton
+ fun provideFavoriteDao(
+ database: ParkingLotDatabase
+ ): FavoriteDao = database.favoriteDao
+
+ @Provides
+ @Singleton
+ fun provideHistoryDao(
+ database: ParkingLotDatabase
+ ): HistoryDao = database.historyDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt
new file mode 100644
index 0000000..4698fd8
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/di/NetworkModule.kt
@@ -0,0 +1,50 @@
+package com.a1573595.parkingdemo.di
+
+import android.content.Context
+import com.a1573595.parkingdemo.BuildConfig
+import com.a1573595.parkingdemo.common.Constants
+import com.a1573595.parkingdemo.data.network.ParkingLotApi
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+ @Provides
+ @Singleton
+ fun provideOkHttpClient(
+ interceptor: HttpLoggingInterceptor,
+ @ApplicationContext context: Context
+ ): OkHttpClient {
+ return OkHttpClient.Builder()
+ .addInterceptor(interceptor)
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
+ return HttpLoggingInterceptor().apply {
+ level = if (BuildConfig.DEBUG) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+ }
+ }
+
+ @Provides
+ @Singleton
+ fun providesParkingLotApi(client: OkHttpClient): ParkingLotApi = Retrofit.Builder()
+ .client(client)
+ .baseUrl(Constants.BASE_URL)
+ .build()
+ .create(ParkingLotApi::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt b/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt
new file mode 100644
index 0000000..2e3492a
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/di/RepositoryModule.kt
@@ -0,0 +1,15 @@
+package com.a1573595.parkingdemo.di
+
+import com.a1573595.parkingdemo.data.repository.ParkingLotRepositoryImpl
+import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+object RepositoryModule {
+ @Provides
+ fun provideParkingLotRepository(newsRepositoryImpl: ParkingLotRepositoryImpl): ParkingLotRepository = newsRepositoryImpl
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt
new file mode 100644
index 0000000..dbc97ee
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/domain/model/ParkingLot.kt
@@ -0,0 +1,44 @@
+package com.a1573595.parkingdemo.domain.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.a1573595.parkingdemo.common.LatLngConverter
+import com.a1573595.parkingdemo.data.model.Park
+
+@Entity
+data class ParkingLot(
+ @PrimaryKey val id: String,
+ val area: String,
+ val name: String,
+ val summary: String,
+ val address: String,
+ val tel: String,
+ val payEx: String,
+ val lat: Double,
+ val lon: Double,
+ val totalCar: Int,
+ val totalMotor: Int,
+ val totalBike: Int,
+ val totalBus: Int,
+) {
+ companion object {
+ fun fromPark(park: Park): ParkingLot {
+ val latlonPair = LatLngConverter.twd97ToLonLat(park.tw97x!!, park.tw97y!!)
+ return ParkingLot(
+ id = park.id!!,
+ area = park.area ?: "Unknown Area",
+ name = park.name ?: "Unknown Name",
+ summary = park.summary ?: "No Summary Available",
+ address = park.address ?: "No Address Available",
+ tel = park.tel ?: "No Tel Available",
+ payEx = park.payEx ?: "No PayEx Available",
+ lat = latlonPair.first,
+ lon = latlonPair.second,
+ totalCar = park.totalCar ?: 0,
+ totalMotor = park.totalMotor ?: 0,
+ totalBike = park.totalBike ?: 0,
+ totalBus = park.totalBus ?: 0,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt
new file mode 100644
index 0000000..2daef54
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/domain/repository/ParkingLotRepository.kt
@@ -0,0 +1,36 @@
+package com.a1573595.parkingdemo.domain.repository
+
+import androidx.paging.PagingData
+import com.a1573595.parkingdemo.data.local.Favorite
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import kotlinx.coroutines.flow.Flow
+
+interface ParkingLotRepository {
+ val lastUpdateTimeFlow: Flow
+
+ val parkingLotListFlow: Flow>
+
+ val favoriteParkingLotListFlow: Flow>
+
+ val historyParkingLotListFlow: Flow>
+
+ suspend fun fetchParkingLotDataSet(): Unit
+
+ fun searchParkingLotPagingDataFlow(
+ keyword: String,
+ hasBus: Boolean,
+ hasCar: Boolean,
+ hasMotor: Boolean,
+ hasBike: Boolean,
+ ): Flow>
+
+ suspend fun getParkingLotById(id: String): ParkingLot?
+
+ fun getFavoriteByIdFlow(id: String): Flow
+
+ suspend fun upsertFavoriteById(id: String): Unit
+
+ suspend fun deleteFavoriteById(id: String): Unit
+
+ suspend fun deleteHistoryById(id: String): Unit
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt
new file mode 100644
index 0000000..8a9f1c3
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/FavoriteUseCase.kt
@@ -0,0 +1,22 @@
+package com.a1573595.parkingdemo.domain.usecase
+
+import com.a1573595.parkingdemo.data.local.Favorite
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import javax.inject.Inject
+
+class FavoriteUseCase @Inject constructor(
+ private val parkingLotRepository: ParkingLotRepository,
+) {
+ operator fun invoke(): Flow> =
+ parkingLotRepository.favoriteParkingLotListFlow.flowOn(Dispatchers.IO)
+
+ fun getById(id: String): Flow = parkingLotRepository.getFavoriteByIdFlow(id).flowOn(Dispatchers.IO)
+
+ suspend fun upsertById(id: String): Unit = parkingLotRepository.upsertFavoriteById(id)
+
+ suspend fun deleteById(id: String): Unit = parkingLotRepository.deleteFavoriteById(id)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt
new file mode 100644
index 0000000..945668a
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/HistoryUseCase.kt
@@ -0,0 +1,17 @@
+package com.a1573595.parkingdemo.domain.usecase
+
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import javax.inject.Inject
+
+class HistoryUseCase @Inject constructor(
+ private val parkingLotRepository: ParkingLotRepository,
+) {
+ operator fun invoke(): Flow> =
+ parkingLotRepository.historyParkingLotListFlow.flowOn(Dispatchers.IO)
+
+ suspend fun deleteById(id: String): Unit = parkingLotRepository.deleteHistoryById(id)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt
new file mode 100644
index 0000000..80efbc3
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/domain/usecase/ParkingLotUseCase.kt
@@ -0,0 +1,39 @@
+package com.a1573595.parkingdemo.domain.usecase
+
+import androidx.paging.PagingData
+import com.a1573595.parkingdemo.common.dateFormatIso8601
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.repository.ParkingLotRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import java.util.Date
+import javax.inject.Inject
+
+class ParkingLotUseCase @Inject constructor(
+ private val parkingLotRepository: ParkingLotRepository,
+) {
+ val lastUpdateTimeFlow: Flow
+ get() = parkingLotRepository.lastUpdateTimeFlow.map {
+ it?.let { value ->
+ dateFormatIso8601.format(Date(value))
+ }
+ }.flowOn(Dispatchers.IO)
+
+ operator fun invoke(): Flow> = parkingLotRepository.parkingLotListFlow.flowOn(Dispatchers.IO)
+
+ suspend fun fetchDataSet(): Unit = parkingLotRepository.fetchParkingLotDataSet()
+
+ fun searchPagingDataFlow(
+ keyword: String = "",
+ hasBus: Boolean = false,
+ hasCar: Boolean = false,
+ hasMotor: Boolean = false,
+ hasBike: Boolean = false,
+ ): Flow> =
+ parkingLotRepository.searchParkingLotPagingDataFlow(keyword, hasBus, hasCar, hasMotor, hasBike)
+ .flowOn(Dispatchers.IO)
+
+ suspend fun getParkingLotById(id: String): ParkingLot? = parkingLotRepository.getParkingLotById(id)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt
new file mode 100644
index 0000000..7a34bda
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/MainActivity.kt
@@ -0,0 +1,24 @@
+package com.a1573595.parkingdemo.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import com.a1573595.parkingdemo.ui.component.DoubleBackPress
+import com.a1573595.parkingdemo.ui.navigation.NavGraph
+import com.a1573595.parkingdemo.ui.theme.ParkingLotTheme
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ ParkingLotTheme {
+ DoubleBackPress()
+ NavGraph()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt
new file mode 100644
index 0000000..37d052d
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/BorderCard.kt
@@ -0,0 +1,24 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun BorderCard(content: @Composable ColumnScope.() -> Unit) {
+ Card(
+ border = BorderStroke(
+ Dimens.dp1,
+ Color.Gray,
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ ),
+ content = content,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt
new file mode 100644
index 0000000..7d8ea03
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/DoubleBackPress.kt
@@ -0,0 +1,45 @@
+package com.a1573595.parkingdemo.ui.component
+
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.a1573595.parkingdemo.R
+
+import kotlinx.coroutines.delay
+
+sealed class BackPress {
+ data object Idle : BackPress()
+ data object InitialTouch : BackPress()
+}
+
+@Composable
+fun DoubleBackPress() {
+ var showToast by remember { mutableStateOf(false) }
+ var backPressState by remember { mutableStateOf(BackPress.Idle) }
+
+ val context = LocalContext.current
+
+ if(showToast){
+ Toast.makeText(context, stringResource(R.string.press_again_to_exit), Toast.LENGTH_SHORT).show()
+ showToast= false
+ }
+
+ LaunchedEffect(key1 = backPressState) {
+ if (backPressState == BackPress.InitialTouch) {
+ delay(2000)
+ backPressState = BackPress.Idle
+ }
+ }
+
+ BackHandler(backPressState == BackPress.Idle) {
+ backPressState = BackPress.InitialTouch
+ showToast = true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt
new file mode 100644
index 0000000..79db8ed
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ErrorBody.kt
@@ -0,0 +1,43 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+
+@Composable
+fun ErrorBody(throwable: Throwable) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Icon(
+ Icons.Filled.Error,
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(.2f),
+ )
+ Text(
+ text = throwable.message ?: "Unknown Error",
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+fun ErrorBodyPreview() {
+ ErrorBody(Exception("Preview"))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt
new file mode 100644
index 0000000..560ba3a
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadMoreFooter.kt
@@ -0,0 +1,22 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun LoadMoreFooter() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(Dimens.dp16),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt
new file mode 100644
index 0000000..92a3651
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/LoadingBody.kt
@@ -0,0 +1,26 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+
+@Composable
+fun LoadingBody() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(modifier = Modifier.fillMaxWidth(.2f))
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+fun LoadingBodyPreview() {
+ LoadingBody()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt
new file mode 100644
index 0000000..240649b
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NavigationAppBar.kt
@@ -0,0 +1,38 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBackIosNew
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NavigationAppBar(
+ onBackClick: () -> Unit,
+ title: String,
+) {
+ TopAppBar(
+ colors = topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ ),
+ navigationIcon = {
+ IconButton(
+ onClick = onBackClick
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBackIosNew,
+ contentDescription = "back",
+ )
+ }
+ },
+ title = {
+ Text(text = title)
+ },
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt
new file mode 100644
index 0000000..7036ecb
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/NoMoreFooter.kt
@@ -0,0 +1,27 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun NoMoreFooter() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(Dimens.dp16),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.no_more),
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt
new file mode 100644
index 0000000..cf2a219
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotItem.kt
@@ -0,0 +1,71 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+
+@Composable
+fun ParkingLotItem(
+ parkingLot: ParkingLot,
+ onClick: (String) -> Unit,
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ elevation = CardDefaults.cardElevation(4.dp),
+ onClick = { onClick(parkingLot.id) }
+ ) {
+ Text(
+ text = parkingLot.name,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ text = "${parkingLot.area}${parkingLot.address}",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = stringResource(R.string.bus_number, parkingLot.totalBus),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = stringResource(R.string.car_number, parkingLot.totalCar),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = stringResource(R.string.motor_number, parkingLot.totalMotor),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = stringResource(R.string.bike_number, parkingLot.totalBike),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt
new file mode 100644
index 0000000..09b9c6e
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/ParkingLotLazyColumn.kt
@@ -0,0 +1,75 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.paging.LoadState
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.itemKey
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun ParkingLotLazyColumn(
+ lazyListState: LazyListState,
+ parkingLotList: LazyPagingItems,
+ onParkingLotItemClick: (String) -> Unit,
+) {
+ LazyColumn(
+ state = lazyListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = Dimens.dp16)
+ .padding(horizontal = Dimens.dp16),
+ ) {
+ items(
+ count = parkingLotList.itemCount,
+ key = parkingLotList.itemKey { it.id }
+ ) { index ->
+ parkingLotList[index]?.let {
+ ParkingLotItem(
+ it,
+ onClick = onParkingLotItemClick,
+ )
+ }
+ }
+ parkingLotList.loadState.apply {
+ when {
+ append is LoadState.Loading -> item { LoadMoreFooter() }
+ refresh is LoadState.NotLoading && append is LoadState.NotLoading -> item { NoMoreFooter() }
+ }
+ }
+ }
+}
+
+@Composable
+fun ParkingLotLazyColumn(
+ lazyListState: LazyListState,
+ parkingLotList: List,
+ onDelete: (String) -> Unit,
+ onClick: (String) -> Unit,
+) {
+ LazyColumn(
+ state = lazyListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = Dimens.dp16),
+ ) {
+ items(
+ count = parkingLotList.size,
+ key = { index -> parkingLotList[index].id }
+ ) { index ->
+ SwipeToDismissContainer(
+ onDelete = { onDelete(parkingLotList[index].id) },
+ ) {
+ ParkingLotItem(
+ parkingLotList[index],
+ onClick = onClick,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt
new file mode 100644
index 0000000..5a01cc4
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/component/SwipeToDismissContainer.kt
@@ -0,0 +1,103 @@
+package com.a1573595.parkingdemo.ui.component
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SwipeToDismissBox
+import androidx.compose.material3.SwipeToDismissBoxValue
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.material3.rememberSwipeToDismissBoxState
+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.graphics.vector.ImageVector
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SwipeToDismissContainer(
+ onDelete: () -> Unit,
+ content: @Composable RowScope.() -> Unit,
+) {
+ val swipeState = rememberSwipeToDismissBoxState(
+ confirmValueChange = { newState ->
+ newState == SwipeToDismissBoxValue.EndToStart
+ },
+ )
+
+ val icon: ImageVector
+ val alignment: Alignment
+ val color: Color
+
+ when (swipeState.dismissDirection) {
+ SwipeToDismissBoxValue.EndToStart -> {
+ icon = Icons.Outlined.Delete
+ alignment = Alignment.CenterEnd
+ color = Color.Red
+ }
+
+ SwipeToDismissBoxValue.StartToEnd -> {
+ icon = Icons.Outlined.Edit
+ alignment = Alignment.CenterStart
+ color = Color.Green.copy(alpha = 0.3f)
+ }
+
+ SwipeToDismissBoxValue.Settled -> {
+ icon = Icons.Outlined.Delete
+ alignment = Alignment.CenterEnd
+// color = MaterialTheme.colorScheme.errorContainer
+ color = Color.Transparent
+ }
+ }
+
+ when (swipeState.currentValue) {
+ SwipeToDismissBoxValue.EndToStart -> {
+ onDelete()
+ }
+
+ SwipeToDismissBoxValue.StartToEnd -> {
+// LaunchedEffect(swipeState) {
+// onEdit()
+// swipeState.snapTo(SwipeToDismissBoxValue.Settled)
+// }
+ }
+
+ SwipeToDismissBoxValue.Settled -> {}
+ }
+
+ SwipeToDismissBox(
+ state = swipeState,
+ enableDismissFromStartToEnd = false,
+ enableDismissFromEndToStart = true,
+ modifier = Modifier.animateContentSize(),
+ backgroundContent = {
+ Box(
+ modifier = Modifier
+ .padding(Dimens.dp8)
+ .fillMaxSize()
+ .background(color),
+ contentAlignment = alignment,
+ ) {
+ Icon(
+ modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .size(Dimens.dp32),
+ imageVector = icon,
+ contentDescription = null,
+ tint = Color.White,
+ )
+ }
+ },
+ content = content,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt
new file mode 100644
index 0000000..d99dd9f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavGraph.kt
@@ -0,0 +1,73 @@
+package com.a1573595.parkingdemo.ui.navigation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.a1573595.parkingdemo.ui.screen.HomeScreen
+import com.a1573595.parkingdemo.ui.screen.detail.DetailScreen
+import com.a1573595.parkingdemo.ui.screen.favorite.FavoriteScreen
+import com.a1573595.parkingdemo.ui.screen.history.HistoryScreen
+import com.a1573595.parkingdemo.ui.screen.map.MapScreen
+import com.a1573595.parkingdemo.ui.screen.search.SearchScreen
+
+@Composable
+fun NavGraph() {
+ val navController = rememberNavController()
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ NavHost(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f),
+ navController = navController,
+ startDestination = NavRoute.Home.route,
+ ) {
+ val onParkingLotItemClick: (String) -> Unit = {
+ navController.navigate(NavRoute.Detail.passParkingLotId(it))
+ }
+
+ composable(NavRoute.Home.route) {
+ HomeScreen(
+ onMapClick = { navController.navigate(NavRoute.Map.route) },
+ onSearchClick = { navController.navigate(NavRoute.Search.route) },
+ onFavoriteClick = { navController.navigate(NavRoute.Favorite.route) },
+ onHistoryClick = { navController.navigate(NavRoute.History.route) },
+ )
+ }
+ composable(NavRoute.Map.route) {
+ MapScreen(
+ onBackClick = { navController.popBackStack() },
+ onParkingLotItemClick = onParkingLotItemClick,
+ )
+ }
+ composable(NavRoute.Search.route) {
+ SearchScreen(
+ onBackClick = { navController.popBackStack() },
+ onParkingLotItemClick = onParkingLotItemClick,
+ )
+ }
+ composable(NavRoute.Favorite.route) {
+ FavoriteScreen(
+ onBackClick = { navController.popBackStack() },
+ onParkingLotItemClick = onParkingLotItemClick,
+ )
+ }
+ composable(NavRoute.History.route) {
+ HistoryScreen(
+ onBackClick = { navController.popBackStack() },
+ onParkingLotItemClick = onParkingLotItemClick,
+ )
+ }
+ composable(NavRoute.Detail.route) {
+ DetailScreen(
+ onBackClick = { navController.popBackStack() },
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt
new file mode 100644
index 0000000..68e9f7b
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/navigation/NavRoute.kt
@@ -0,0 +1,20 @@
+package com.a1573595.parkingdemo.ui.navigation
+
+sealed class NavRoute(val route: String) {
+ companion object {
+ const val KEY_ID = "id"
+ }
+
+ data object Home : NavRoute("home")
+ data object Map : NavRoute("map")
+ data object Search : NavRoute("search")
+ data object Favorite : NavRoute("favorite")
+ data object History : NavRoute("history")
+ data object Detail : NavRoute("detail/{$KEY_ID}") {
+ fun passParkingLotId(id: String): String {
+ return this.route.replace(
+ oldValue = "{$KEY_ID}", newValue = id
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt
new file mode 100644
index 0000000..05abf47
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeScreen.kt
@@ -0,0 +1,228 @@
+package com.a1573595.parkingdemo.ui.screen
+
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.component.ErrorBody
+import com.a1573595.parkingdemo.ui.component.LoadingBody
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun HomeScreen(
+ onMapClick: () -> Unit,
+ onSearchClick: () -> Unit,
+ onFavoriteClick: () -> Unit,
+ onHistoryClick: () -> Unit,
+ viewModel: HomeViewModel = hiltViewModel(),
+) {
+ val uiState = viewModel.uiState.value
+
+ Scaffold(
+ topBar = {
+ HomeAppBar(
+ isRefreshAble = uiState.isSuccess,
+ onRefreshClick = {
+ viewModel.refreshData()
+ },
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = Dimens.dp16)
+ ) {
+ when {
+ uiState.isLoading -> LoadingBody()
+ uiState.isError -> ErrorBody(throwable = uiState.requireError)
+ else -> HomeBody(
+ uiState = uiState.requireValue,
+ onMapClick = onMapClick,
+ onSearchClick = onSearchClick,
+ onFavoriteClick = onFavoriteClick,
+ onHistoryClick = onHistoryClick,
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeAppBar(
+ isRefreshAble: Boolean,
+ onRefreshClick: () -> Unit,
+) {
+ TopAppBar(
+ colors = topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ ),
+ title = {
+ Text(
+ stringResource(R.string.parking_lot),
+ style = MaterialTheme.typography.headlineMedium,
+ )
+ },
+ actions = {
+ if (isRefreshAble) {
+ IconButton(onClick = { onRefreshClick() }) {
+ Icon(
+ imageVector = Icons.Filled.Refresh,
+ contentDescription = null,
+ )
+ }
+ }
+ },
+ )
+}
+
+@Composable
+fun HomeBody(
+ uiState: HomeUiState,
+ onMapClick: () -> Unit,
+ onSearchClick: () -> Unit,
+ onFavoriteClick: () -> Unit,
+ onHistoryClick: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = Dimens.dp16)
+ ) {
+ Text(
+ text = stringResource(
+ R.string.total_of_parking_lots_download_at,
+ uiState.numberOfParkingLots,
+ uiState.lastUpTime
+ ),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Image(
+ painterResource(R.mipmap.ic_launcher_foreground),
+ contentDescription = "",
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize(),
+ contentScale = ContentScale.Crop,
+ )
+ ImageElevatedButton(
+ onClick = onMapClick,
+ backgroundColor = android.R.color.holo_green_light,
+ drawableId = R.drawable.ic_map,
+ title = stringResource(R.string.map)
+ )
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(Dimens.dp16),
+ )
+ ImageElevatedButton(
+ onClick = onSearchClick,
+ backgroundColor = android.R.color.holo_blue_light,
+ drawableId = R.drawable.ic_list,
+ title = stringResource(R.string.search)
+ )
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(Dimens.dp16),
+ )
+ ImageElevatedButton(
+ onClick = onFavoriteClick,
+ backgroundColor = android.R.color.holo_red_light,
+ drawableId = R.drawable.ic_favorite,
+ title = stringResource(R.string.favorite)
+ )
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(Dimens.dp16),
+ )
+ ImageElevatedButton(
+ onClick = onHistoryClick,
+ backgroundColor = android.R.color.holo_orange_light,
+ drawableId = R.drawable.ic_history,
+ title = stringResource(R.string.history)
+ )
+ }
+}
+
+@Composable
+fun ImageElevatedButton(
+ onClick: () -> Unit,
+ @ColorRes backgroundColor: Int,
+ @DrawableRes drawableId: Int,
+ title: String,
+) {
+ ElevatedButton(
+ onClick = onClick,
+ modifier = Modifier.fillMaxWidth(),
+ elevation = ButtonDefaults.buttonElevation(
+ defaultElevation = Dimens.dp4,
+ ),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = colorResource(id = backgroundColor),
+ )
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Image(
+ modifier = Modifier.height(32.dp),
+ painter = painterResource(id = drawableId),
+ contentDescription = title,
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun ImageElevatedButtonPreview() {
+ ImageElevatedButton(
+ onClick = { },
+ backgroundColor = android.R.color.holo_green_light,
+ drawableId = R.drawable.ic_map,
+ title = "Map Mode",
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt
new file mode 100644
index 0000000..9cd731f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeUiState.kt
@@ -0,0 +1,6 @@
+package com.a1573595.parkingdemo.ui.screen
+
+data class HomeUiState(
+ val lastUpTime: String,
+ val numberOfParkingLots: Int,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt
new file mode 100644
index 0000000..840cd5e
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/HomeViewModel.kt
@@ -0,0 +1,42 @@
+package com.a1573595.parkingdemo.ui.screen
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.a1573595.parkingdemo.common.AsyncValue
+import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.zip
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class HomeViewModel @Inject constructor(
+ private val parkingLotUseCase: ParkingLotUseCase,
+) : ViewModel() {
+ private val _uiState = mutableStateOf>(AsyncValue.Loading)
+
+ val uiState: State> = _uiState
+
+ init {
+ viewModelScope.launch {
+ parkingLotUseCase.lastUpdateTimeFlow.zip(parkingLotUseCase()) { lastUpdateTime, parkingLotList ->
+ if (lastUpdateTime.isNullOrEmpty()) {
+ refreshData()
+ } else {
+ _uiState.value = AsyncValue.Data(HomeUiState(lastUpdateTime, parkingLotList.size))
+ }
+ }.catch {
+ _uiState.value = AsyncValue.Error(it)
+ }.collect()
+ }
+ }
+
+ fun refreshData() = viewModelScope.launch {
+ _uiState.value = AsyncValue.Loading
+ parkingLotUseCase.fetchDataSet()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt
new file mode 100644
index 0000000..1a2a822
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailScreen.kt
@@ -0,0 +1,229 @@
+package com.a1573595.parkingdemo.ui.screen.detail
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.DirectionsBike
+import androidx.compose.material.icons.filled.ArrowBackIosNew
+import androidx.compose.material.icons.filled.DirectionsBus
+import androidx.compose.material.icons.filled.DirectionsCar
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Money
+import androidx.compose.material.icons.filled.Motorcycle
+import androidx.compose.material.icons.filled.Phone
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.a1573595.parkingdemo.common.AsyncValue
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.ui.component.BorderCard
+import com.a1573595.parkingdemo.ui.component.ErrorBody
+import com.a1573595.parkingdemo.ui.component.LoadingBody
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun DetailScreen(
+ onBackClick: () -> Unit,
+ viewModel: DetailViewModel = hiltViewModel(),
+) {
+ val uiState = viewModel.uiState.value
+
+ Scaffold(
+ topBar = {
+ DetailAppBar(
+ uiState = uiState,
+ onBackClick = onBackClick,
+ onFavoriteClick = { viewModel.updateFavorite() },
+ )
+ }
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier.padding(innerPadding)
+ ) {
+ when {
+ uiState.isLoading -> LoadingBody()
+ uiState.isError -> ErrorBody(throwable = uiState.requireError)
+ else -> DetailBody(uiState.requireValue.parkingLot)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DetailAppBar(
+ uiState: AsyncValue,
+ onBackClick: () -> Unit,
+ onFavoriteClick: () -> Unit,
+) {
+ TopAppBar(
+ colors = topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ ),
+ navigationIcon = {
+ IconButton(
+ onClick = onBackClick
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBackIosNew,
+ contentDescription = "back",
+ )
+ }
+ },
+ title = {
+ if (uiState.isSuccess)
+ Text(
+ text = uiState.requireValue.parkingLot.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ actions = {
+ if (uiState.isSuccess)
+ IconButton(onClick = { onFavoriteClick() }) {
+ Icon(
+ imageVector = if (uiState.requireValue.isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
+ contentDescription = "favorite",
+ tint = Color.Red,
+ )
+ }
+ },
+ )
+}
+
+@Composable
+fun DetailBody(parkingLot: ParkingLot) {
+ Column(
+ modifier = Modifier.padding(Dimens.dp16),
+ ) {
+ Text(
+ text = parkingLot.name,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Text(
+ text = "${parkingLot.area} ${parkingLot.address}",
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.height(Dimens.dp16))
+ BorderCard {
+ Column(
+ modifier = Modifier.padding(
+ horizontal = Dimens.dp8,
+ vertical = Dimens.dp16,
+ ),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround,
+ ) {
+ Icon(
+ imageVector = Icons.Filled.DirectionsBus,
+ contentDescription = null,
+ modifier = Modifier.size(Dimens.dp48),
+ )
+ Icon(
+ imageVector = Icons.Filled.DirectionsCar,
+ contentDescription = null,
+ modifier = Modifier.size(Dimens.dp48),
+ )
+ Icon(
+ imageVector = Icons.Filled.Motorcycle,
+ contentDescription = null,
+ modifier = Modifier.size(Dimens.dp48),
+ )
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.DirectionsBike,
+ contentDescription = null,
+ modifier = Modifier.size(Dimens.dp48),
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = "${parkingLot.totalBus}",
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = "${parkingLot.totalCar}",
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = "${parkingLot.totalMotor}",
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ Text(
+ text = "${parkingLot.totalBike}",
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(Dimens.dp32))
+ Row {
+ Icon(
+ imageVector = Icons.Filled.Phone,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ Text(
+ text = parkingLot.tel,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ Spacer(modifier = Modifier.height(Dimens.dp32))
+ Row {
+ Icon(
+ imageVector = Icons.Filled.Info,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ Text(
+ text = parkingLot.summary,
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
+ Spacer(modifier = Modifier.height(Dimens.dp32))
+ Row {
+ Icon(
+ imageVector = Icons.Filled.Money,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ Text(
+ text = parkingLot.payEx,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt
new file mode 100644
index 0000000..b10481b
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailUiState.kt
@@ -0,0 +1,8 @@
+package com.a1573595.parkingdemo.ui.screen.detail
+
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+
+data class DetailUiState(
+ val isFavorite: Boolean,
+ val parkingLot: ParkingLot,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt
new file mode 100644
index 0000000..1640cd3
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/detail/DetailViewModel.kt
@@ -0,0 +1,52 @@
+package com.a1573595.parkingdemo.ui.screen.detail
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.a1573595.parkingdemo.common.AsyncValue
+import com.a1573595.parkingdemo.domain.usecase.FavoriteUseCase
+import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase
+import com.a1573595.parkingdemo.ui.navigation.NavRoute
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class DetailViewModel @Inject constructor(
+ private val handle: SavedStateHandle,
+ private val parkingLotUseCase: ParkingLotUseCase,
+ private val favoriteUseCase: FavoriteUseCase,
+) : ViewModel() {
+ private val id = handle.get(NavRoute.KEY_ID)!!
+
+ private val _uiState: MutableState> = mutableStateOf(AsyncValue.Loading)
+
+ val uiState: State> = _uiState
+
+ init {
+ viewModelScope.launch {
+ parkingLotUseCase.getParkingLotById(id)!!.let {
+ _uiState.value = AsyncValue.Data(DetailUiState(false, it))
+ }
+
+ favoriteUseCase.getById(id).collect {
+ _uiState.value = AsyncValue.Data(_uiState.value.requireValue.copy(isFavorite = it != null))
+ }
+ }
+ }
+
+ fun updateFavorite() {
+ val isFavorite = _uiState.value.requireValue.isFavorite
+
+ viewModelScope.launch {
+ if (isFavorite) {
+ favoriteUseCase.deleteById(id)
+ } else {
+ favoriteUseCase.upsertById(id)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt
new file mode 100644
index 0000000..799123a
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteScreen.kt
@@ -0,0 +1,84 @@
+package com.a1573595.parkingdemo.ui.screen.favorite
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.VerticalAlignTop
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.component.NavigationAppBar
+import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun FavoriteScreen(
+ onBackClick: () -> Unit,
+ onParkingLotItemClick: (String) -> Unit,
+ viewModel: FavoriteViewModel = hiltViewModel(),
+) {
+ Scaffold(
+ topBar = {
+ NavigationAppBar(
+ onBackClick = onBackClick,
+ title = stringResource(id = R.string.favorite),
+ )
+ },
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(vertical = Dimens.dp16)
+ ) {
+ val scrollToTop = remember { mutableStateOf(false) }
+ val lazyListState: LazyListState = rememberLazyListState()
+
+ LaunchedEffect(key1 = scrollToTop.value) {
+ if (scrollToTop.value) {
+ scrollToTop.value = false
+ lazyListState.scrollToItem(0)
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val parkingLotList = viewModel.parkingLotListFlow.collectAsState(initial = emptyList())
+ ParkingLotLazyColumn(
+ lazyListState, parkingLotList.value,
+ onDelete = { viewModel.deleteById(it) },
+ onClick = onParkingLotItemClick,
+ )
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(Dimens.dp32),
+ onClick = {
+ scrollToTop.value = true
+ },
+ ) {
+ Icon(
+ Icons.Filled.VerticalAlignTop,
+ modifier = Modifier.size(Dimens.dp32),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt
new file mode 100644
index 0000000..fdced4d
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/favorite/FavoriteViewModel.kt
@@ -0,0 +1,21 @@
+package com.a1573595.parkingdemo.ui.screen.favorite
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.usecase.FavoriteUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class FavoriteViewModel @Inject constructor(
+ private val favoriteUseCase: FavoriteUseCase,
+) : ViewModel() {
+ val parkingLotListFlow: Flow> = favoriteUseCase()
+
+ fun deleteById(id: String) = viewModelScope.launch {
+ favoriteUseCase.deleteById(id)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt
new file mode 100644
index 0000000..288144a
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryScreen.kt
@@ -0,0 +1,84 @@
+package com.a1573595.parkingdemo.ui.screen.history
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.VerticalAlignTop
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.component.NavigationAppBar
+import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun HistoryScreen(
+ onBackClick: () -> Unit,
+ onParkingLotItemClick: (String) -> Unit,
+ viewModel: HistoryViewModel = hiltViewModel(),
+) {
+ Scaffold(
+ topBar = {
+ NavigationAppBar(
+ onBackClick = onBackClick,
+ title = stringResource(id = R.string.history),
+ )
+ },
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(vertical = Dimens.dp16)
+ ) {
+ val scrollToTop = remember { mutableStateOf(false) }
+ val lazyListState: LazyListState = rememberLazyListState()
+
+ LaunchedEffect(key1 = scrollToTop.value) {
+ if (scrollToTop.value) {
+ scrollToTop.value = false
+ lazyListState.scrollToItem(0)
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val parkingLotList = viewModel.parkingLotListFlow.collectAsState(initial = emptyList())
+ ParkingLotLazyColumn(
+ lazyListState, parkingLotList.value,
+ onDelete = { viewModel.deleteById(it) },
+ onClick = onParkingLotItemClick,
+ )
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(Dimens.dp32),
+ onClick = {
+ scrollToTop.value = true
+ },
+ ) {
+ Icon(
+ Icons.Filled.VerticalAlignTop,
+ modifier = Modifier.size(Dimens.dp32),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt
new file mode 100644
index 0000000..2c1bd5f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/history/HistoryViewModel.kt
@@ -0,0 +1,21 @@
+package com.a1573595.parkingdemo.ui.screen.history
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.a1573595.parkingdemo.domain.usecase.HistoryUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class HistoryViewModel @Inject constructor(
+ private val historyUseCase: HistoryUseCase,
+) : ViewModel() {
+ val parkingLotListFlow: Flow> = historyUseCase()
+
+ fun deleteById(id: String) = viewModelScope.launch {
+ historyUseCase.deleteById(id)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt
new file mode 100644
index 0000000..39ff06c
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapScreen.kt
@@ -0,0 +1,109 @@
+package com.a1573595.parkingdemo.ui.screen.map
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.component.NavigationAppBar
+import com.a1573595.parkingdemo.ui.screen.map.bean.ParkingLotCluster
+import com.google.android.gms.maps.model.CameraPosition
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.MapUiSettings
+import com.google.maps.android.compose.MapsComposeExperimentalApi
+import com.google.maps.android.compose.clustering.Clustering
+import com.google.maps.android.compose.clustering.rememberClusterManager
+import com.google.maps.android.compose.rememberCameraPositionState
+
+@OptIn(MapsComposeExperimentalApi::class)
+@Composable
+fun MapScreen(
+ onBackClick: () -> Unit,
+ onParkingLotItemClick: (String) -> Unit,
+ viewModel: MapViewModel = hiltViewModel(),
+) {
+ val uiSettings by remember { mutableStateOf(MapUiSettings()) }
+ val cameraPositionState = rememberCameraPositionState {
+ val taipeiLatLng = LatLng(
+ 25.0329694,
+ 121.56541770000001
+ )
+ position = CameraPosition.fromLatLngZoom(taipeiLatLng, 15f)
+ }
+
+ Scaffold(
+ topBar = {
+ NavigationAppBar(
+ onBackClick = onBackClick,
+ title = stringResource(id = R.string.map),
+ )
+ },
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ ) {
+ GoogleMap(
+ modifier = Modifier
+ .fillMaxSize(),
+ uiSettings = uiSettings,
+ cameraPositionState = cameraPositionState,
+ ) {
+ val configuration = LocalConfiguration.current
+ val screenHeight = configuration.screenHeightDp.dp
+ val screenWidth = configuration.screenWidthDp.dp
+ val clusterManager = rememberClusterManager()
+
+ val parkingLotList = viewModel.parkingLotClusterListFlow.collectAsState(initial = emptyList())
+
+ SideEffect {
+ clusterManager?.setAlgorithm(
+ NonHierarchicalViewBasedAlgorithm(
+ screenWidth.value.toInt(),
+ screenHeight.value.toInt(),
+ )
+ )
+
+ clusterManager?.setOnClusterClickListener {
+ val zoom = cameraPositionState.position.zoom
+ cameraPositionState.position = CameraPosition.fromLatLngZoom(it.position, zoom + 1)
+ false
+ }
+
+ clusterManager?.setOnClusterItemInfoWindowClickListener {
+ onParkingLotItemClick(it.id)
+ }
+ }
+
+ val items = remember { mutableStateListOf() }
+ LaunchedEffect(parkingLotList.value) {
+ items.addAll(parkingLotList.value)
+ }
+
+ if (clusterManager != null) {
+ Clustering(
+ items = items,
+ clusterManager = clusterManager,
+ )
+ }
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt
new file mode 100644
index 0000000..ea04a16
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/MapViewModel.kt
@@ -0,0 +1,18 @@
+package com.a1573595.parkingdemo.ui.screen.map
+
+import androidx.lifecycle.ViewModel
+import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase
+import com.a1573595.parkingdemo.ui.screen.map.bean.ParkingLotCluster
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+@HiltViewModel
+class MapViewModel @Inject constructor(
+ private val parkingLotUseCase: ParkingLotUseCase,
+) : ViewModel() {
+ val parkingLotClusterListFlow: Flow> = parkingLotUseCase().map {
+ it.map { ParkingLotCluster.fromParkingLot(it) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt
new file mode 100644
index 0000000..e139121
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/map/bean/ParkingLotCluster.kt
@@ -0,0 +1,29 @@
+package com.a1573595.parkingdemo.ui.screen.map.bean
+
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.clustering.ClusterItem
+
+data class ParkingLotCluster(
+ private val _position: LatLng,
+ val id: String,
+ val name: String,
+ val description: String,
+) : ClusterItem {
+ companion object {
+ fun fromParkingLot(park: ParkingLot): ParkingLotCluster = ParkingLotCluster(
+ _position = LatLng(park.lat, park.lon),
+ id = park.id,
+ name = park.name,
+ description = "${park.area}${park.address}",
+ )
+ }
+
+ override fun getPosition(): LatLng = _position
+
+ override fun getTitle(): String = name
+
+ override fun getSnippet(): String = description
+
+ override fun getZIndex(): Float? = null
+}
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt
new file mode 100644
index 0000000..9a8a3eb
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/FilterType.kt
@@ -0,0 +1,8 @@
+package com.a1573595.parkingdemo.ui.screen.search
+
+enum class FilterType {
+ BUS,
+ CAR,
+ MOTOR,
+ BIKE,
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt
new file mode 100644
index 0000000..f9effe6
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchScreen.kt
@@ -0,0 +1,233 @@
+package com.a1573595.parkingdemo.ui.screen.search
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.VerticalAlignTop
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.paging.LoadState
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.a1573595.parkingdemo.R
+import com.a1573595.parkingdemo.ui.component.ErrorBody
+import com.a1573595.parkingdemo.ui.component.LoadingBody
+import com.a1573595.parkingdemo.ui.component.NavigationAppBar
+import com.a1573595.parkingdemo.ui.component.ParkingLotLazyColumn
+import com.a1573595.parkingdemo.ui.theme.Dimens
+
+@Composable
+fun SearchScreen(
+ onBackClick: () -> Unit,
+ onParkingLotItemClick: (String) -> Unit,
+ viewModel: SearchViewModel = hiltViewModel(),
+) {
+ Scaffold(
+ topBar = {
+ NavigationAppBar(
+ onBackClick = onBackClick,
+ title = stringResource(id = R.string.search),
+ )
+ },
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(vertical = Dimens.dp16)
+ ) {
+ val scrollToTop = remember { mutableStateOf(false) }
+ val lazyListState: LazyListState = rememberLazyListState()
+
+ val uiState = viewModel.uiState.value
+
+ LaunchedEffect(key1 = scrollToTop.value) {
+ if (scrollToTop.value) {
+ scrollToTop.value = false
+ lazyListState.scrollToItem(0)
+ }
+ }
+
+ TextFieldSearchBar(
+ backgroundColor = Color.Transparent,
+ value = uiState.keyword,
+ onClear = {
+ viewModel.updateKeyword("")
+ },
+ onValueChange = {
+ viewModel.updateKeyword(it)
+ },
+ )
+ Row(modifier = Modifier.padding(horizontal = Dimens.dp16)) {
+ FilterChip(
+ selected = uiState.hasBus,
+ modifier = Modifier.weight(1f),
+ onClick = { viewModel.updateFilter(FilterType.BUS) },
+ label = {
+ Text(
+ text = stringResource(id = R.string.bus),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ FilterChip(
+ selected = uiState.hasCar,
+ modifier = Modifier.weight(1f),
+ onClick = { viewModel.updateFilter(FilterType.CAR) },
+ label = {
+ Text(
+ text = stringResource(id = R.string.car),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ FilterChip(
+ selected = uiState.hasMotor,
+ modifier = Modifier.weight(1f),
+ onClick = { viewModel.updateFilter(FilterType.MOTOR) },
+ label = {
+ Text(
+ text = stringResource(id = R.string.motor),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ )
+ Spacer(modifier = Modifier.width(Dimens.dp16))
+ FilterChip(
+ selected = uiState.hasBike,
+ modifier = Modifier.weight(1f),
+ onClick = { viewModel.updateFilter(FilterType.BIKE) },
+ label = {
+ Text(
+ text = stringResource(id = R.string.bike),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ },
+ )
+ }
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ uiState.parkingLotPagingDataFlow.collectAsLazyPagingItems().let {
+ with(it.loadState.refresh) {
+ if (this is LoadState.Loading) {
+ LoadingBody()
+ } else if (this is LoadState.Error) {
+ ErrorBody(this.error)
+ }
+ }
+ ParkingLotLazyColumn(lazyListState, it, onParkingLotItemClick)
+ }
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(Dimens.dp32),
+ onClick = {
+ scrollToTop.value = true
+ },
+ ) {
+ Icon(
+ Icons.Filled.VerticalAlignTop,
+ modifier = Modifier.size(Dimens.dp32),
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TextFieldSearchBar(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color,
+ value: String,
+ onClear: () -> Unit,
+ onValueChange: (String) -> Unit,
+) {
+ val interactionSource = remember {
+ MutableInteractionSource()
+ }
+
+ TextField(
+ interactionSource = interactionSource,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = Dimens.dp16)
+ .border(
+ width = Dimens.dp1,
+ color = Color.Black,
+ shape = MaterialTheme.shapes.medium,
+ ),
+ shape = MaterialTheme.shapes.medium,
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = backgroundColor,
+ unfocusedContainerColor = backgroundColor,
+ disabledIndicatorColor = backgroundColor,
+ errorIndicatorColor = backgroundColor,
+ focusedIndicatorColor = backgroundColor,
+ unfocusedIndicatorColor = backgroundColor,
+ ),
+ singleLine = true,
+ leadingIcon = {
+ Icon(
+ Icons.Filled.Search,
+ modifier = Modifier.size(Dimens.dp32),
+ contentDescription = null,
+ )
+ },
+ trailingIcon = {
+ if (value.isNotEmpty()) {
+ Icon(
+ Icons.Filled.Clear,
+ modifier = Modifier
+ .size(Dimens.dp32)
+ .clickable { onClear() },
+ contentDescription = null,
+ )
+ }
+ },
+ placeholder = {
+ Text(
+ text = stringResource(R.string.keyword),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ value = value,
+ onValueChange = onValueChange,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt
new file mode 100644
index 0000000..488d5e3
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchUiState.kt
@@ -0,0 +1,14 @@
+package com.a1573595.parkingdemo.ui.screen.search
+
+import androidx.paging.PagingData
+import com.a1573595.parkingdemo.domain.model.ParkingLot
+import kotlinx.coroutines.flow.Flow
+
+data class SearchUiState(
+ val keyword: String,
+ val hasBus: Boolean = false,
+ val hasCar: Boolean = false,
+ val hasMotor: Boolean = false,
+ val hasBike: Boolean = false,
+ val parkingLotPagingDataFlow: Flow>,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt
new file mode 100644
index 0000000..b6131a8
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/screen/search/SearchViewModel.kt
@@ -0,0 +1,58 @@
+package com.a1573595.parkingdemo.ui.screen.search
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.paging.cachedIn
+import com.a1573595.parkingdemo.domain.usecase.ParkingLotUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.debounce
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchViewModel @Inject constructor(
+ private val parkingLotUseCase: ParkingLotUseCase,
+) : ViewModel() {
+ private val _uiState = mutableStateOf(
+ SearchUiState(
+ keyword = "",
+ parkingLotPagingDataFlow = parkingLotUseCase.searchPagingDataFlow().cachedIn(viewModelScope)
+ )
+ )
+
+ val uiState: State = _uiState
+
+ fun updateKeyword(value: String) {
+ _uiState.value = uiState.value.copy(keyword = value)
+
+ updateFlow()
+ }
+
+ fun updateFilter(filterType: FilterType) {
+ when (filterType) {
+ FilterType.BUS -> _uiState.value = uiState.value.copy(hasBus = !uiState.value.hasBus)
+ FilterType.CAR -> _uiState.value = uiState.value.copy(hasCar = !uiState.value.hasCar)
+ FilterType.MOTOR -> _uiState.value = uiState.value.copy(hasMotor = !uiState.value.hasMotor)
+ FilterType.BIKE -> _uiState.value = uiState.value.copy(hasBike = !uiState.value.hasBike)
+ }
+
+ updateFlow()
+ }
+
+ @OptIn(FlowPreview::class)
+ private fun updateFlow() {
+ val state = uiState.value
+
+ parkingLotUseCase.searchPagingDataFlow(
+ state.keyword,
+ state.hasBus,
+ state.hasCar,
+ state.hasMotor,
+ state.hasBike,
+ ).debounce(1000).cachedIn(viewModelScope).let {
+ _uiState.value = uiState.value.copy(parkingLotPagingDataFlow = it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt
new file mode 100644
index 0000000..c94419f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.a1573595.parkingdemo.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt
new file mode 100644
index 0000000..3d2a691
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Dimens.kt
@@ -0,0 +1,17 @@
+package com.a1573595.parkingdemo.ui.theme
+
+import androidx.compose.ui.unit.dp
+
+object Dimens {
+ val dp1 = 1.dp
+ val dp2 = 2.dp
+ val dp4 = 4.dp
+ val dp8 = 8.dp
+ val dp12 = 12.dp
+ val dp16 = 16.dp
+ val dp20 = 20.dp
+ val dp24 = 24.dp
+ val dp32 = 32.dp
+ val dp48 = 48.dp
+ val dp64 = 64.dp
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt
new file mode 100644
index 0000000..ad4106c
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Theme.kt
@@ -0,0 +1,57 @@
+package com.a1573595.parkingdemo.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun ParkingLotTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt
new file mode 100644
index 0000000..5863c1f
--- /dev/null
+++ b/app/src/main/java/com/a1573595/parkingdemo/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.a1573595.parkingdemo.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt
deleted file mode 100644
index 2b373c7..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/BaseActivity.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.content.Context
-import android.content.res.Resources
-import androidx.appcompat.app.AppCompatActivity
-
-abstract class BaseActivity : AppCompatActivity() {
- override fun attachBaseContext(base: Context) {
- val configuration = base.resources.configuration
-
- if (configuration.fontScale != 1.0f) {
- configuration.fontScale = 1.0f
- super.attachBaseContext(base.createConfigurationContext(configuration))
- return
- }
-
- super.attachBaseContext(base)
- }
-
- override fun getResources(): Resources {
- val resources = super.getResources()
-
- if (resources?.configuration?.fontScale != 1.0f) {
- val configuration = resources.configuration
- configuration.fontScale = 1.0f
-
- val context = createConfigurationContext(configuration)
- return context.resources
- }
-
- return resources
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt
deleted file mode 100644
index 2711a1f..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/BaseViewModel.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
-import io.reactivex.rxjava3.disposables.CompositeDisposable
-import io.reactivex.rxjava3.disposables.Disposable
-
-abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
- private val disposable: CompositeDisposable = CompositeDisposable()
-
- internal fun addDisposable(d: Disposable) {
- disposable.add(d)
- }
-
- override fun onCleared() {
- disposable.clear()
- super.onCleared()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt b/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt
deleted file mode 100644
index 7f6a617..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/MainApplication.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import android.content.Context
-import android.content.res.Resources
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.datastore.preferences.core.stringPreferencesKey
-import com.a1573595.parkinglotdemo.database.ParkingLotDataStore
-import com.a1573595.parkinglotdemo.database.ParkingLotDatabase
-import io.reactivex.rxjava3.core.Single
-import io.reactivex.rxjava3.plugins.RxJavaPlugins
-import timber.log.Timber
-import java.util.*
-
-class MainApplication : Application() {
- override fun onCreate() {
- super.onCreate()
-
- ParkingLotDataStore.build(this)
-
- ParkingLotDataStore.ds.updateDataAsync {
- val mutablePreferences = it.toMutablePreferences()
- val password = it[stringPreferencesKey("DatabaseKey")] ?: UUID.randomUUID().toString()
- mutablePreferences[stringPreferencesKey("DatabaseKey")] = password
-
- ParkingLotDatabase.build(this, password)
-
- Single.just(mutablePreferences)
- }
-
- if (BuildConfig.DEBUG) {
- Timber.plant(Timber.DebugTree())
- }
-
- RxJavaPlugins.setErrorHandler {
- Timber.e(it)
- }
-
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- }
-
- override fun attachBaseContext(base: Context) {
- val configuration = base.resources.configuration
-
- if (configuration.fontScale != 1.0f) {
- configuration.fontScale = 1.0f
- super.attachBaseContext(base.createConfigurationContext(configuration))
- return
- }
-
- super.attachBaseContext(base)
- }
-
- override fun getResources(): Resources {
- val resources = super.getResources()
-
- if (resources?.configuration?.fontScale != 1.0f) {
- val configuration = resources.configuration
- configuration.fontScale = 1.0f
-
- val context = createConfigurationContext(configuration)
- return context.resources
- }
-
- return resources
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt b/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt
deleted file mode 100644
index ba1eb67..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/api/ApiInterface.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.a1573595.parkinglotdemo.api
-
-import io.reactivex.rxjava3.core.Single
-import okhttp3.ResponseBody
-import retrofit2.http.GET
-import retrofit2.http.Url
-
-fun interface ApiInterface {
- @GET
- fun downloadFileWithDynamicUrlSync(@Url fileUrl: String): Single
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt b/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt
deleted file mode 100644
index 6bb9f44..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/api/NetWorkService.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.a1573595.parkinglotdemo.api
-
-import com.a1573595.parkinglotdemo.BuildConfig
-import okhttp3.*
-import okhttp3.logging.HttpLoggingInterceptor
-import retrofit2.Retrofit
-import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
-import java.util.concurrent.TimeUnit
-
-object NetWorkService {
- val apiInterface: ApiInterface
-
- init {
- val logger = HttpLoggingInterceptor()
- logger.level =
- if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
-
- val client = OkHttpClient.Builder()
- .connectTimeout(15, TimeUnit.SECONDS)
- .readTimeout(15, TimeUnit.SECONDS)
- .writeTimeout(15, TimeUnit.SECONDS)
- .addInterceptor(logger)
- .build()
-
- val retrofit = Retrofit.Builder()
- .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
- .baseUrl("https://tcgbusfs.blob.core.windows.net/blobtcmsv/")
- .client(client)
- .build()
-
- apiInterface = retrofit.create(ApiInterface::class.java)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt
deleted file mode 100644
index 99f9398..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/Favorite.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity(tableName = TABLE_FAVORITE)
-data class Favorite(
- @PrimaryKey
- val id: String,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt
deleted file mode 100644
index 67324f7..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/FavoriteDao.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
-
-@Dao
-interface FavoriteDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(item: Favorite): Completable
-
- @Query("SELECT * FROM TABLE_FAVORITE WHERE id LIKE :id")
- fun getByID(id: String): Single
-
- @Query("SELECT * FROM TABLE_FAVORITE INNER JOIN TABLE_PARKING_LOT ON TABLE_FAVORITE.id = TABLE_PARKING_LOT.id")
- fun getLoveList(): Single>
-
- @Query("DELETE FROM TABLE_FAVORITE WHERE id LIKE :id")
- fun deleteByID(id: String): Completable
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt
deleted file mode 100644
index 4b0e9b0..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/History.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity(tableName = TABLE_HISTORY)
-data class History(
- @PrimaryKey
- val id: String,
- var hashTag: Long = System.currentTimeMillis()
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt
deleted file mode 100644
index 7a03277..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/HistoryDao.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
-
-@Dao
-interface HistoryDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(item: History): Completable
-
- @Query("SELECT * FROM TABLE_HISTORY INNER JOIN TABLE_PARKING_LOT ON TABLE_HISTORY.id = TABLE_PARKING_LOT.id ORDER BY hashTag DESC")
- fun getHistoryList(): Single>
-
- @Query("DELETE FROM TABLE_HISTORY WHERE id LIKE :id")
- fun deleteByID(id: String): Completable
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt
deleted file mode 100644
index 5c99833..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLot.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.recyclerview.widget.DiffUtil
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity(tableName = TABLE_PARKING_LOT)
-data class ParkingLot(
- @PrimaryKey
- val id: String,
- var area: String?,
- var name: String?,
- var summary: String?,
- var address: String?,
- var tel: String?,
- var payex: String?,
- var totalcar: Int = 0,
- var totalmotor: Int = 0,
- var totalbike: Int = 0,
- var totalbus: Int = 0,
- var lat: Double = 0.0,
- var lng: Double = 0.0,
-)
-
-class ParkingLotCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: ParkingLot, newItem: ParkingLot): Boolean {
- return oldItem.id == newItem.id
- }
-
- override fun areContentsTheSame(oldItem: ParkingLot, newItem: ParkingLot): Boolean {
- return oldItem == newItem
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt
deleted file mode 100644
index 41b5a06..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDao.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import androidx.room.*
-import androidx.sqlite.db.SupportSQLiteQuery
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
-
-@Dao
-interface ParkingLotDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(item: ParkingLot): Completable
-
- @Query("SELECT * FROM TABLE_PARKING_LOT WHERE id LIKE :id")
- fun getByID(id: String): Single
-
- @Query("DELETE FROM TABLE_PARKING_LOT WHERE id LIKE :id")
- fun deleteByID(id: String): Completable
-
- @Transaction
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insertAll(parkingLots: List): Single>
-
- @Query("SELECT * FROM TABLE_PARKING_LOT")
- fun getAll(): Single>
-
- @Query("SELECT * FROM TABLE_PARKING_LOT WHERE name LIKE '%' || :name || '%'")
- fun getAllByName(name: String): Single>
-
- @RawQuery
- fun getAllByQuery(query: SupportSQLiteQuery): Single>
-
- @Query("DELETE FROM TABLE_PARKING_LOT")
- fun deleteAll(): Completable
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt
deleted file mode 100644
index 591ac1b..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDataStore.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import android.content.Context
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.longPreferencesKey
-import androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder
-import androidx.datastore.rxjava3.RxDataStore
-
-private const val DS_NAME = "settings"
-
-val UPDATE_TIME = longPreferencesKey("update_time")
-
-object ParkingLotDataStore {
- private lateinit var _ds: RxDataStore
-
- val ds: RxDataStore
- get() = _ds
-
- fun build(context: Context) {
- _ds = RxPreferenceDataStoreBuilder(context, DS_NAME).build()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt b/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt
deleted file mode 100644
index 2173582..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/database/ParkingLotDatabase.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.a1573595.parkinglotdemo.database
-
-import android.content.Context
-import androidx.room.Database
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import net.sqlcipher.database.SupportFactory
-
-private const val DB_NAME = "PK.db"
-private const val DB_VERSION = 1
-
-const val TABLE_PARKING_LOT = "Table_Parking_Lot"
-const val TABLE_FAVORITE = "Table_Favorite"
-const val TABLE_HISTORY = "Table_History"
-
-@Database(
- entities = [ParkingLot::class, Favorite::class, History::class],
- version = DB_VERSION,
- exportSchema = false
-)
-abstract class ParkingLotDatabase : RoomDatabase() {
- companion object {
- lateinit var instance: ParkingLotDatabase
-
- @Synchronized
- fun build(context: Context, password: String) {
- instance = Room.databaseBuilder(
- context,
- ParkingLotDatabase::class.java,
- DB_NAME
- )
- .openHelperFactory(SupportFactory(password.toByteArray()))
- .fallbackToDestructiveMigration()
- .build()
- }
- }
-
- abstract fun getParkingDao(): ParkingLotDao
-
- abstract fun getFavoriteDao(): FavoriteDao
-
- abstract fun getHistoryDao(): HistoryDao
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt b/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt
deleted file mode 100644
index 2a8c1ad..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/model/TaipeiParkingLotInfo.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.a1573595.parkinglotdemo.model
-
-import com.google.gson.annotations.SerializedName
-
-class TaipeiParkingLotInfo {
- @SerializedName("data")
- val data: Data = Data()
-
- class Data {
- @SerializedName("park")
- val park: Array = emptyArray()
-
- class Park {
- @SerializedName("id")
- var id: String? = null
- @SerializedName("area")
- var area : String? = null
- @SerializedName("name")
- var name : String? = null
- @SerializedName("summary")
- var summary : String? = null
- @SerializedName("address")
- var address : String? = null
- @SerializedName("tel")
- var tel : String? = null
- @SerializedName("payex")
- var payex : String? = null
- @SerializedName("tw97x")
- var tw97x = 0.0
- @SerializedName("tw97y")
- var tw97y = 0.0
- @SerializedName("totalcar")
- var totalcar = 0
- @SerializedName("totalmotor")
- var totalmotor = 0
- @SerializedName("totalbike")
- var totalbike = 0
- @SerializedName("totalbus")
- var totalbus = 0
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt
deleted file mode 100644
index 5858e76..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailActivity.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.a1573595.parkinglotdemo.page.detail
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.view.MenuItem
-import android.view.animation.Animation
-import android.view.animation.ScaleAnimation
-import androidx.activity.viewModels
-import com.a1573595.parkinglotdemo.BaseActivity
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.databinding.ActivityDetailBinding
-
-class DetailActivity : BaseActivity() {
- companion object {
- private const val KEY_ID = "id"
-
- fun startActivity(context: Context, id: String) {
- val intent = Intent(context, DetailActivity::class.java)
- val bundle = Bundle()
- bundle.putString(KEY_ID, id)
- intent.putExtras(bundle)
- context.startActivity(intent)
- }
- }
-
- private val viewModel: DetailViewModel by viewModels()
-
- private lateinit var binding: ActivityDetailBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = ActivityDetailBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
-
- subscriptViewModel()
- viewModel.loadData(intent.extras?.getString(KEY_ID))
- }
-
- private fun subscriptViewModel() {
- viewModel.parkingLotEvent.observe(this) {
- val parkingLot = it.peekContent()
- supportActionBar?.title = parkingLot.name
-
- binding.tvName.text = parkingLot.name
- binding.tvAddress.text = parkingLot.address
- binding.tvArea.text = parkingLot.area
- binding.tvPhone.text = parkingLot.tel
- binding.tvInfo.text = parkingLot.summary
- binding.tvRate.text = parkingLot.payex
- binding.tvBus.text = java.lang.String.valueOf(parkingLot.totalbus)
- binding.tvCar.text = java.lang.String.valueOf(parkingLot.totalcar)
- binding.tvMoto.text = java.lang.String.valueOf(parkingLot.totalmotor)
- binding.tvBike.text = java.lang.String.valueOf(parkingLot.totalbike)
-
- binding.imgFavorite.setOnClickListener {
- viewModel.addFavorite(parkingLot.id)
- }
- }
-
- viewModel.isFavoriteEvent.observeForever {
- binding.imgFavorite.setImageResource(if (it.peekContent()) R.drawable.ic_favorite else R.drawable.ic_unfavorite)
-
- val scaleAnimation = ScaleAnimation(
- 1.0f, 1.2f, 1.0f, 1.2f,
- Animation.RELATIVE_TO_SELF, .5f,
- Animation.RELATIVE_TO_SELF, .5f
- )
- scaleAnimation.duration = 300
-
- binding.imgFavorite.startAnimation(scaleAnimation)
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> onBackPressedDispatcher.onBackPressed()
- }
- return super.onOptionsItemSelected(item)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt
deleted file mode 100644
index 6f73215..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/detail/DetailViewModel.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.a1573595.parkinglotdemo.page.detail
-
-import android.app.Application
-import androidx.lifecycle.MutableLiveData
-import com.a1573595.parkinglotdemo.BaseViewModel
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import com.a1573595.parkinglotdemo.tool.Event
-
-class DetailViewModel : BaseViewModel {
- constructor(application: Application) : super(application) {
- repository = ParkingLotRepository()
- }
-
- constructor(application: Application, repository: ParkingLotRepository) : super(application) {
- this.repository = repository
- }
-
- private val repository: ParkingLotRepository
-
- private var id: String = ""
-
- val parkingLotEvent: MutableLiveData> = MutableLiveData()
- val isFavoriteEvent: MutableLiveData> = MutableLiveData()
-
- fun loadData(id: String?) {
- id?.let {
- this.id = it
- }
-
- addDisposable(repository.getFavorites(this.id).subscribe({
- isFavoriteEvent.postValue(Event(true))
- }, {
- isFavoriteEvent.postValue(Event(false))
- }))
-
- addDisposable(repository.getParkingLot(this.id)
- .subscribe { list ->
- parkingLotEvent.postValue(Event(list))
- }
- )
- }
-
- fun addFavorite(id: String) {
- if (isFavoriteEvent.value?.peekContent() == true) {
- addDisposable(repository.deleteFavorite(id).subscribe {
- isFavoriteEvent.postValue(Event(false))
- })
- } else {
- addDisposable(repository.addFavorite(id).subscribe {
- isFavoriteEvent.postValue(Event(true))
- })
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt
deleted file mode 100644
index 5740dfb..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchActivity.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.a1573595.parkinglotdemo.page.fuzzySearch
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.view.MenuItem
-import androidx.activity.viewModels
-import androidx.core.widget.addTextChangedListener
-import com.a1573595.parkinglotdemo.BaseActivity
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.databinding.ActivityFuzzySearchBinding
-
-class FuzzySearchActivity : BaseActivity() {
- companion object {
- fun startActivity(context: Context) {
- context.startActivity(Intent(context, FuzzySearchActivity::class.java))
- }
- }
-
- private val viewModel: FuzzySearchViewModel by viewModels()
-
- private lateinit var binding: ActivityFuzzySearchBinding
-
- private val adapter: FuzzySearchAdapter = FuzzySearchAdapter()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = ActivityFuzzySearchBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
-
- binding.recyclerView.adapter = adapter
-
- binding.edSearch.addTextChangedListener {
- viewModel.setKeyword(binding.edSearch.text.toString())
- }
-
- binding.groupTransportation.setOnCheckedStateChangeListener { _, checkedIds ->
- val mode = when (checkedIds.firstOrNull()) {
- R.id.chip_bus -> 0
- R.id.chip_car -> 1
- R.id.chip_moto -> 2
- R.id.chip_bike -> 3
- else -> 1
- }
-
- viewModel.setMode(mode)
- }
-
- subscriptViewModel()
- viewModel.loadDataSet()
- }
-
- private fun subscriptViewModel() {
- viewModel.dataSetEvent.observe(this) {
- adapter.submitList(it.peekContent())
- binding.recyclerView.postDelayed({
- binding.recyclerView.layoutManager?.scrollToPosition(0)
- }, 200)
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> onBackPressedDispatcher.onBackPressed()
- }
- return super.onOptionsItemSelected(item)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt
deleted file mode 100644
index 3738237..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchAdapter.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.a1573595.parkinglotdemo.page.fuzzySearch
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotCallback
-import com.a1573595.parkinglotdemo.databinding.ItemParkingLotBinding
-import com.a1573595.parkinglotdemo.page.detail.DetailActivity
-
-internal class FuzzySearchAdapter :
- ListAdapter(ParkingLotCallback()) {
- internal inner class Holder(private val binding: ItemParkingLotBinding) :
- RecyclerView.ViewHolder(binding.root) {
- init {
- itemView.setOnClickListener {
- DetailActivity.startActivity(it.context, getItem(adapterPosition).id)
- }
- }
-
- fun bind(parkingLot: ParkingLot) {
- binding.tvName.text = parkingLot.name
- binding.tvAddress.text = parkingLot.address
- binding.tvTotal.text = parkingLot.address
-
- binding.tvTotal.text = binding.tvTotal.context.getString(
- R.string.transportation,
- parkingLot.totalbus,
- parkingLot.totalcar,
- parkingLot.totalmotor,
- parkingLot.totalbike
- )
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
- return Holder(
- ItemParkingLotBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- override fun onBindViewHolder(holder: Holder, position: Int) {
- holder.bind(getItem(position))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt
deleted file mode 100644
index c8e79d8..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/fuzzySearch/FuzzySearchViewModel.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.a1573595.parkinglotdemo.page.fuzzySearch
-
-import android.app.Application
-import androidx.lifecycle.MutableLiveData
-import com.a1573595.parkinglotdemo.BaseViewModel
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import com.a1573595.parkinglotdemo.tool.Event
-
-class FuzzySearchViewModel : BaseViewModel {
- constructor(application: Application) : super(application) {
- repository = ParkingLotRepository()
- }
-
- constructor(application: Application, repository: ParkingLotRepository) : super(application) {
- this.repository = repository
- }
-
- private val repository: ParkingLotRepository
-
- private var mode = 1
- private var keyword = ""
-
- val dataSetEvent: MutableLiveData>> = MutableLiveData()
-
- fun setKeyword(keyword: String) {
- this.keyword = keyword
-
- loadDataSet()
- }
-
- fun setMode(mode: Int) {
- this.mode = mode
-
- loadDataSet()
- }
-
- fun loadDataSet() {
- var query = "SELECT * FROM TABLE_PARKING_LOT WHERE "
-
- query += when (mode) {
- 0 -> "totalbus > 0"
- 1 -> "totalcar > 0"
- 2 -> "totalmotor > 0"
- else -> "totalbike > 0"
- }
-
- if (keyword.isNotEmpty()) {
- query += String.format(
- " AND (name LIKE '%%%s%%' OR address LIKE '%%%s%%')",
- keyword,
- keyword
- )
- }
-
- addDisposable(repository.searchParkingLots(query).subscribe { list ->
- dataSetEvent.postValue(Event(list))
- })
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt
deleted file mode 100644
index 7a60fcc..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryActivity.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-package com.a1573595.parkinglotdemo.page.history
-
-import android.content.Context
-import android.content.Intent
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Paint
-import android.graphics.RectF
-import android.os.Bundle
-import android.view.MenuItem
-import androidx.activity.viewModels
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.BlendModeColorFilterCompat
-import androidx.core.graphics.BlendModeCompat
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.RecyclerView
-import com.a1573595.parkinglotdemo.BaseActivity
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.databinding.ActivityHistoryBinding
-import com.google.android.material.snackbar.Snackbar
-
-class HistoryActivity : BaseActivity() {
- companion object {
- private const val KET_MODE = "mode"
-
- const val MODE_FAVORITE = 1
- const val MODE_HISTORY = 2
-
- fun startActivity(context: Context, mode: Int) {
- val intent = Intent(context, HistoryActivity::class.java)
- val bundle = Bundle()
- bundle.putInt(KET_MODE, mode)
- intent.putExtras(bundle)
- context.startActivity(intent)
- }
- }
-
- private val viewModel: HistoryModel by viewModels()
-
- private lateinit var binding: ActivityHistoryBinding
-
- private val adapter: HistoryAdapter = HistoryAdapter()
- private val textPaint = Paint()
- private val backgroundPaint = Paint()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = ActivityHistoryBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
-
- textPaint.color = ContextCompat.getColor(
- this@HistoryActivity,
- android.R.color.white
- )
- textPaint.textSize = 72f
- textPaint.style = Paint.Style.FILL
- textPaint.textAlign = Paint.Align.CENTER
- backgroundPaint.color = ContextCompat.getColor(
- this@HistoryActivity,
- android.R.color.holo_red_light
- )
-
- val itemTouchHelper = ItemTouchHelper(callback)
- itemTouchHelper.attachToRecyclerView(binding.recyclerView)
- binding.recyclerView.adapter = adapter
-
- subscriptViewModel()
- intent.extras?.getInt(KET_MODE)?.let {
- viewModel.setMode(it)
- }
- }
-
- private fun subscriptViewModel() {
- viewModel.dataSetEvent.observe(this) {
- adapter.submitList(it.peekContent())
- }
- }
-
- override fun onResume() {
- super.onResume()
-
- viewModel.loadDataSet()
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> onBackPressedDispatcher.onBackPressed()
- }
- return super.onOptionsItemSelected(item)
- }
-
- private val callback: ItemTouchHelper.Callback = object : ItemTouchHelper.SimpleCallback(
- ItemTouchHelper.UP or ItemTouchHelper.DOWN,
- ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
- ) {
- override fun getMovementFlags(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder
- ): Int {
- val dragFlags = 0
- val swipeFlags = ItemTouchHelper.LEFT
- return makeMovementFlags(dragFlags, swipeFlags)
- }
-
- override fun onChildDraw(
- c: Canvas,
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- dX: Float,
- dY: Float,
- actionState: Int,
- isCurrentlyActive: Boolean
- ) {
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
- val icon = ContextCompat.getDrawable(this@HistoryActivity, R.drawable.ic_delete)!!
- icon.colorFilter =
- BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
- Color.WHITE,
- BlendModeCompat.SRC_ATOP
- )
-
- val itemView = viewHolder.itemView
-
- val rectF: RectF
-
- val multiple = 1f
- val iconMargin = (itemView.height - icon.intrinsicHeight) / 2
- val iconTop = itemView.top + (iconMargin * multiple).toInt()
- val iconBottom = iconTop + (icon.intrinsicHeight / multiple).toInt()
- if (dX > 0) { // left
- rectF = RectF(
- itemView.left.toFloat(),
- itemView.top.toFloat(),
- itemView.left + dX,
- itemView.bottom.toFloat()
- )
-
- val iconLeft = itemView.left + iconMargin
- val iconRight = iconLeft + (icon.intrinsicWidth / multiple).toInt()
- icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
- } else { // right
- rectF = RectF(
- itemView.right + dX,
- itemView.top.toFloat(),
- itemView.right.toFloat(),
- itemView.bottom.toFloat()
- )
-
- val iconRight = itemView.right - iconMargin
- val iconLeft = iconRight - (icon.intrinsicWidth / multiple).toInt()
- icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
- }
-
- c.drawRect(rectF, backgroundPaint)
- icon.draw(c)
-// c.drawText(
-// "Delete",
-// rectf.right - (textPaint.textSize * 3), // rectf.centerX()
-// rectf.centerY() + (textPaint.textSize / 2),
-// textPaint
-// )
- }
-
- override fun onMove(
- recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- return false
- }
-
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- val position: Int = viewHolder.adapterPosition
- viewModel.delete(position)
-
- Snackbar.make(binding.root, R.string.delete, Snackbar.LENGTH_LONG)
- .setAction(R.string.recover) { viewModel.undoDelete() }
- .show()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt
deleted file mode 100644
index 9cd873a..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryAdapter.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.a1573595.parkinglotdemo.page.history
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotCallback
-import com.a1573595.parkinglotdemo.databinding.ItemParkingLotBinding
-import com.a1573595.parkinglotdemo.page.detail.DetailActivity
-
-internal class HistoryAdapter : ListAdapter(
- ParkingLotCallback()
-) {
- internal inner class Holder(private val binding: ItemParkingLotBinding) :
- RecyclerView.ViewHolder(binding.root) {
- init {
- itemView.setOnClickListener {
- DetailActivity.startActivity(it.context, getItem(adapterPosition).id)
- }
- }
-
- fun bind(parkingLot: ParkingLot) {
- binding.tvName.text = parkingLot.name
- binding.tvAddress.text = parkingLot.address
- binding.tvTotal.text = binding.tvTotal.context.getString(
- R.string.transportation,
- parkingLot.totalbus,
- parkingLot.totalcar,
- parkingLot.totalmotor,
- parkingLot.totalbike
- )
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
- return Holder(
- ItemParkingLotBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- override fun onBindViewHolder(holder: Holder, position: Int) {
- holder.bind(getItem(position))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt
deleted file mode 100644
index 3db5572..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/history/HistoryModel.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.a1573595.parkinglotdemo.page.history
-
-import android.app.Application
-import androidx.lifecycle.MutableLiveData
-import com.a1573595.parkinglotdemo.BaseViewModel
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import com.a1573595.parkinglotdemo.tool.Event
-
-class HistoryModel : BaseViewModel {
- constructor(application: Application) : super(application) {
- repository = ParkingLotRepository()
- }
-
- constructor(application: Application, repository: ParkingLotRepository) : super(application) {
- this.repository = repository
- }
-
- private val repository: ParkingLotRepository
-
- private var mode: Int = 0
-
- val dataSetEvent: MutableLiveData>> = MutableLiveData()
-
- private var lastDeleteID: String? = null
-
- fun setMode(mode: Int) {
- this.mode = mode
- }
-
- fun loadDataSet() {
- val completable = if (mode == HistoryActivity.MODE_FAVORITE) {
- repository.getFavorites()
- } else {
- repository.getHistory()
- }
-
- addDisposable(completable.subscribe { list -> dataSetEvent.postValue(Event(list)) })
- }
-
- fun delete(position: Int) {
- dataSetEvent.value?.peekContent()?.let {
- lastDeleteID = it[position].id
-
- val completable = if (mode == HistoryActivity.MODE_FAVORITE) {
- repository.deleteFavorite(it[position].id)
- } else {
- repository.deleteHistory(it[position].id)
- }
-
- addDisposable(completable.subscribe { loadDataSet() })
- }
- }
-
- fun undoDelete() {
- lastDeleteID?.let {
- val completable = if (mode == HistoryActivity.MODE_FAVORITE) {
- repository.addFavorite(it)
- } else {
- repository.addHistory(it)
- }
-
- addDisposable(completable.subscribe { loadDataSet() })
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt
deleted file mode 100644
index f5397b1..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainActivity.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.a1573595.parkinglotdemo.page.main
-
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.Toast
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.addCallback
-import androidx.activity.viewModels
-import com.a1573595.parkinglotdemo.BaseActivity
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.databinding.ActivityMainBinding
-import com.a1573595.parkinglotdemo.page.history.HistoryActivity
-import com.a1573595.parkinglotdemo.page.fuzzySearch.FuzzySearchActivity
-import com.a1573595.parkinglotdemo.page.map.MapActivity
-
-class MainActivity : BaseActivity() {
- private val viewModel: MainViewModel by viewModels()
-
- private lateinit var binding: ActivityMainBinding
-
- private val backHandler = Handler(Looper.getMainLooper())
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- registerOnBackPress()
-
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
- setSupportActionBar(binding.toolbar)
-
- subscriptViewModel()
- viewModel.loadDataSet()
- }
-
- private fun subscriptViewModel() {
- viewModel.dataSetEvent.observe(this) {
- binding.tvDataset.text = getString(R.string.total_of_data_set, it.peekContent().size)
-
- setListen()
- }
-
- viewModel.updateTimeEvent.observe(this) {
- binding.tvUpdateTime.text = getString(R.string.download_at, it.peekContent())
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- val inflater = menuInflater
- inflater.inflate(R.menu.menu_update, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.menu_update -> {
- binding.tvDataset.text = getString(R.string.downloading)
- viewModel.updateDataSet()
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun registerOnBackPress() {
- onBackPressedDispatcher.addCallback {
- if (backHandler.hasMessages(0)) {
- finish()
- } else {
- Toast.makeText(this@MainActivity, getString(R.string.press_again_to_exit), Toast.LENGTH_SHORT)
- .show()
- backHandler.removeCallbacksAndMessages(null)
- backHandler.postDelayed({}, 2000)
- }
- }
- }
-
- private fun setListen() {
- binding.cardMap.setOnClickListener {
- MapActivity.startActivity(this)
- }
-
- binding.cardList.setOnClickListener {
- FuzzySearchActivity.startActivity(this)
- }
-
- binding.cardFavorite.setOnClickListener {
- HistoryActivity.startActivity(this, HistoryActivity.MODE_FAVORITE)
- }
-
- binding.cardHistory.setOnClickListener {
- HistoryActivity.startActivity(this, HistoryActivity.MODE_HISTORY)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt
deleted file mode 100644
index 1047788..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/main/MainViewModel.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.a1573595.parkinglotdemo.page.main
-
-import android.app.Application
-import androidx.lifecycle.MutableLiveData
-import com.a1573595.parkinglotdemo.BaseViewModel
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import com.a1573595.parkinglotdemo.tool.Event
-import java.text.SimpleDateFormat
-import java.util.*
-
-class MainViewModel : BaseViewModel {
- constructor(application: Application) : super(application) {
- repository = ParkingLotRepository()
- }
-
- constructor(application: Application, repository: ParkingLotRepository) : super(application) {
- this.repository = repository
- }
-
- private val repository: ParkingLotRepository
- val updateTimeEvent: MutableLiveData> = MutableLiveData()
- val dataSetEvent: MutableLiveData>> = MutableLiveData()
-
- fun loadDataSet() {
- addDisposable(
- repository.getUpdateTime()
- .subscribe({
- getParkingLots(it)
- }, {
- updateDataSet()
- })
- )
- }
-
- fun updateDataSet() {
- addDisposable(repository.downloadDataSet()
- .subscribe { _ ->
- loadDataSet()
- }
- )
- }
-
- private fun getParkingLots(updateTime: Long) {
- addDisposable(repository.getParkingLots()
- .subscribe { list ->
- dataSetEvent.postValue(Event(list))
- updateTimeEvent.postValue(Event(calTimeMilliToTime(updateTime)))
- }
- )
- }
-
- private fun calTimeMilliToTime(time: Long): String {
- val date = Date(time)
- val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault())
- return format.format(date)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt
deleted file mode 100644
index 07aba1b..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapActivity.kt
+++ /dev/null
@@ -1,306 +0,0 @@
-package com.a1573595.parkinglotdemo.page.map
-
-import android.content.Context
-import android.content.Intent
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.location.Address
-import android.location.Geocoder
-import android.location.Geocoder.GeocodeListener
-import android.os.Build
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.provider.BaseColumns
-import android.view.*
-import androidx.activity.viewModels
-import androidx.appcompat.widget.SearchView
-import androidx.cursoradapter.widget.CursorAdapter
-import androidx.cursoradapter.widget.SimpleCursorAdapter
-import com.a1573595.parkinglotdemo.BaseActivity
-import com.a1573595.parkinglotdemo.R
-import com.a1573595.parkinglotdemo.databinding.ActivityMapBinding
-import com.a1573595.parkinglotdemo.databinding.DialogParkingLotBinding
-import com.a1573595.parkinglotdemo.page.detail.DetailActivity
-import com.a1573595.parkinglotdemo.tool.ParkingCluster
-import com.google.android.gms.maps.CameraUpdateFactory
-import com.google.android.gms.maps.GoogleMap
-import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener
-import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener
-import com.google.android.gms.maps.OnMapReadyCallback
-import com.google.android.gms.maps.SupportMapFragment
-import com.google.android.gms.maps.model.LatLng
-import com.google.android.gms.maps.model.LatLngBounds
-import com.google.android.gms.maps.model.Marker
-import com.google.android.gms.maps.model.MarkerOptions
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.maps.android.clustering.Cluster
-import com.google.maps.android.clustering.ClusterManager
-import com.google.maps.android.clustering.ClusterManager.OnClusterClickListener
-import com.google.maps.android.clustering.ClusterManager.OnClusterItemClickListener
-import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm
-import com.google.maps.android.clustering.view.DefaultClusterRenderer
-import com.google.maps.android.collections.MarkerManager
-import java.lang.Exception
-import java.util.*
-
-class MapActivity : BaseActivity(), OnMapReadyCallback,
- OnCameraIdleListener, OnMarkerClickListener,
- OnClusterClickListener,
- OnClusterItemClickListener {
- companion object {
- private const val Max_Clustering_Room_Level = 17f
-
- fun startActivity(context: Context) {
- context.startActivity(Intent(context, MapActivity::class.java))
- }
- }
-
- private val viewModel: MapViewModel by viewModels()
-
- private lateinit var binding: ActivityMapBinding
-
- private lateinit var simpleCursorAdapter: SimpleCursorAdapter
-
- private lateinit var mMap: GoogleMap
- private lateinit var mClusterManager: ClusterManager
- private lateinit var normalMarkerManager: MarkerManager.Collection
-
- private var zoomLevel = 0f
-
- private lateinit var geocoder: Geocoder
- private val searchHandler = Handler(Looper.getMainLooper())
-
- private inner class ParkingRender :
- DefaultClusterRenderer(applicationContext, mMap, mClusterManager) {
- override fun shouldRenderAsCluster(cluster: Cluster): Boolean {
- return zoomLevel < Max_Clustering_Room_Level && super.shouldRenderAsCluster(
- cluster
- )
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = ActivityMapBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- window.insetsController?.hide(WindowInsets.Type.statusBars())
- } else {
- window.setFlags(
- WindowManager.LayoutParams.FLAG_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FULLSCREEN
- )
- }
-
- val mapFragment =
- supportFragmentManager.findFragmentById(binding.map.id) as SupportMapFragment
- mapFragment.getMapAsync(this)
-
- subscriptViewModel()
- }
-
- private fun subscriptViewModel() {
- viewModel.dataSetEvent.observe(this) {
- it.peekContent().forEach { parkingLot ->
- mClusterManager.addItem(
- ParkingCluster(
- LatLng(parkingLot.lat, parkingLot.lng), parkingLot.id,
- parkingLot.name, parkingLot.area, parkingLot.totalcar,
- parkingLot.totalmotor, parkingLot.totalbike, parkingLot.totalbus
- )
- )
- }
-
- mClusterManager.cluster()
- mMap.uiSettings.setAllGesturesEnabled(true)
- }
- }
-
- override fun onMapReady(googleMap: GoogleMap) {
- mMap = googleMap
-
- mMap.uiSettings.setAllGesturesEnabled(false)
- initClusterManager()
- initSearchView()
-
- mMap.moveCamera(
- CameraUpdateFactory.newLatLngZoom(
- LatLng(
- 25.0329694,
- 121.56541770000001
- ), 15f
- )
- )
-
- mMap.setOnCameraIdleListener(this)
- mMap.setOnMapLoadedCallback { viewModel.loadDataSet() }
- }
-
- override fun onCameraIdle() {
- zoomLevel = mMap.cameraPosition.zoom
- mClusterManager.onCameraIdle()
- }
-
- override fun onMarkerClick(marker: Marker): Boolean {
- marker.showInfoWindow()
- mMap.moveCamera(CameraUpdateFactory.newLatLng(marker.position))
-
- return true
- }
-
- override fun onClusterClick(cluster: Cluster): Boolean {
- val builder = LatLngBounds.builder()
- for (item in cluster.items) {
- builder.include(item.position)
- }
-
- try {
- val bounds = builder.build()
- mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return true
- }
-
- override fun onClusterItemClick(item: ParkingCluster): Boolean {
- mMap.moveCamera(CameraUpdateFactory.newLatLng(item.position))
- showDialog(item)
- return true
- }
-
- private fun initClusterManager() {
- mClusterManager = ClusterManager(this, mMap)
- val dm = resources.displayMetrics
- mClusterManager.setAlgorithm(
- NonHierarchicalViewBasedAlgorithm(
- dm.widthPixels,
- dm.heightPixels
- )
- )
- mClusterManager.renderer = ParkingRender()
- mClusterManager.setOnClusterClickListener(this)
- mClusterManager.setOnClusterItemClickListener(this)
-
- normalMarkerManager = mClusterManager.markerManager.newCollection()
- normalMarkerManager.setOnMarkerClickListener(this)
- }
-
- private fun initSearchView() {
- geocoder = Geocoder(this, Locale.getDefault())
-
- val from = arrayOf("address", "lat", "lng")
- val to = intArrayOf(android.R.id.text1)
- simpleCursorAdapter = SimpleCursorAdapter(
- this, android.R.layout.simple_list_item_1,
- null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER
- )
- binding.searchView.suggestionsAdapter = simpleCursorAdapter
-
- binding.searchView.setOnClickListener { binding.searchView.isIconified = false }
-
- binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- return false
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- simpleCursorAdapter.changeCursor(null)
- searchHandler.removeCallbacksAndMessages(null)
- searchHandler.postDelayed({
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- geocoder.getFromLocationName(
- binding.searchView.query.toString(),
- 5,
- object : GeocodeListener {
- override fun onGeocode(list: MutableList) {
- handleAddress(list)
- }
-
- override fun onError(errorMessage: String?) {
- handleAddress(emptyList())
- }
- })
- } else {
- val geocodeAddress =
- geocoder.getFromLocationName(binding.searchView.query.toString(), 5)
- ?: emptyList()
- handleAddress(geocodeAddress)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }, 750)
- return false
- }
- })
-
- binding.searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
- override fun onSuggestionSelect(position: Int): Boolean {
- return false
- }
-
- override fun onSuggestionClick(position: Int): Boolean {
- val cursor = simpleCursorAdapter.getItem(position) as Cursor
- val address = cursor.getString(cursor.getColumnIndexOrThrow("address"))
- val lat = cursor.getDouble(cursor.getColumnIndexOrThrow("lat"))
- val lng = cursor.getDouble(cursor.getColumnIndexOrThrow("lng"))
- normalMarkerManager.clear()
- val marker = normalMarkerManager.addMarker(
- MarkerOptions().position(LatLng(lat, lng)).title(address)
- )
- marker.showInfoWindow()
- mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(lat, lng), 15f))
- return false
- }
- })
-
- binding.searchView.queryHint = getString(R.string.query_hint)
- binding.searchView.visibility = View.VISIBLE
- }
-
- private fun showDialog(cluster: ParkingCluster) {
- val binding = DialogParkingLotBinding.inflate(LayoutInflater.from(this)).apply {
- tvName.text = cluster.name
- tvAddress.text = cluster.area
- tvTotal.text = getString(
- R.string.transportation, cluster.totalBus, cluster.totalCar,
- cluster.totalMotor, cluster.totalBike
- )
-
- root.setOnClickListener {
- DetailActivity.startActivity(this@MapActivity, cluster.id)
- }
- }
-
- MaterialAlertDialogBuilder(this)
- .setView(binding.root)
- .show()
- }
-
- private fun handleAddress(geocodeAddress: List) {
- val c = MatrixCursor(
- arrayOf(
- BaseColumns._ID,
- "address",
- "lat",
- "lng"
- )
- )
- for (i in geocodeAddress.indices) {
- c.addRow(
- arrayOf(
- i,
- geocodeAddress[i].getAddressLine(0),
- geocodeAddress[i].latitude,
- geocodeAddress[i].longitude,
- )
- )
- }
- simpleCursorAdapter.changeCursor(c)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt b/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt
deleted file mode 100644
index 8c5cd6b..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/page/map/MapViewModel.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.a1573595.parkinglotdemo.page.map
-
-import android.app.Application
-import androidx.lifecycle.MutableLiveData
-import com.a1573595.parkinglotdemo.BaseViewModel
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import com.a1573595.parkinglotdemo.tool.Event
-
-class MapViewModel : BaseViewModel {
- constructor(application: Application) : super(application) {
- repository = ParkingLotRepository()
- }
-
- constructor(application: Application, repository: ParkingLotRepository) : super(application) {
- this.repository = repository
- }
-
- private val repository: ParkingLotRepository
-
- val dataSetEvent: MutableLiveData>> = MutableLiveData()
-
- fun loadDataSet() {
- addDisposable(repository.getParkingLots().subscribe { list ->
- dataSetEvent.postValue(Event(list))
- })
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt b/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt
deleted file mode 100644
index a3bd64d..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/repository/ParkingLotRepository.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.a1573595.parkinglotdemo.repository
-
-import androidx.datastore.preferences.core.MutablePreferences
-import androidx.sqlite.db.SimpleSQLiteQuery
-import com.a1573595.parkinglotdemo.api.NetWorkService
-import com.a1573595.parkinglotdemo.database.*
-import com.a1573595.parkinglotdemo.model.TaipeiParkingLotInfo
-import com.a1573595.parkinglotdemo.tool.LatLngCoding
-import com.google.gson.Gson
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Flowable
-import io.reactivex.rxjava3.core.Single
-import io.reactivex.rxjava3.schedulers.Schedulers
-import java.io.ByteArrayOutputStream
-import java.util.zip.GZIPInputStream
-
-class ParkingLotRepository {
- fun getUpdateTime(): Flowable =
- ParkingLotDataStore.ds.data().map { it[UPDATE_TIME]!! }
- .subscribeOn(Schedulers.io())
-
- fun downloadDataSet(): Single> =
- NetWorkService.apiInterface.downloadFileWithDynamicUrlSync("TCMSV_alldesc.gz")
- .map { body ->
- val inputStream = body.byteStream()
- val unGzip = GZIPInputStream(inputStream)
-
- val buffer = ByteArray(256)
- val out = ByteArrayOutputStream()
-
- var length: Int
- while (unGzip.read(buffer).also { length = it } >= 0) {
- out.write(buffer, 0, length)
- }
- val info: TaipeiParkingLotInfo =
- Gson().fromJson(out.toString("UTF-8"), TaipeiParkingLotInfo::class.java)
-
- val list: MutableList = mutableListOf()
- var latLng: List
-
- info.data.park.filterNot { it.id.isNullOrEmpty() }.forEach {
- latLng = LatLngCoding.calTWD97ToLonLat(it.tw97x, it.tw97y).split(",")
- list.add(
- ParkingLot(
- it.id!!, it.area, it.name, it.summary, it.address,
- it.tel, it.payex, it.totalcar,
- it.totalmotor, it.totalbike, it.totalbus,
- latLng[0].toDouble(), latLng[1].toDouble()
- )
- )
- }
- list
- }
- .flatMap { deleteDataSet().toSingle { it } }
- .flatMap { writeDataSet(it) }
- .doOnSuccess {
- ParkingLotDataStore.ds.updateDataAsync { prefsIn ->
- val mutablePreferences: MutablePreferences =
- prefsIn.toMutablePreferences()
- mutablePreferences[UPDATE_TIME] = System.currentTimeMillis()
- Single.just(mutablePreferences)
- }.subscribe()
- }
- .subscribeOn(Schedulers.io())
-
- fun deleteDataSet(): Completable =
- ParkingLotDatabase.instance.getParkingDao()
- .deleteAll()
- .subscribeOn(Schedulers.io())
-
- fun writeDataSet(list: List): Single> =
- ParkingLotDatabase.instance.getParkingDao()
- .insertAll(list)
- .subscribeOn(Schedulers.io())
-
- fun getParkingLots(): Single> =
- ParkingLotDatabase.instance.getParkingDao()
- .getAll()
- .subscribeOn(Schedulers.io())
-
- fun getParkingLot(id: String): Single =
- ParkingLotDatabase.instance.getParkingDao()
- .getByID(id)
- .flatMap { addHistory(it.id).toSingle { it } }
- .subscribeOn(Schedulers.io())
-
- fun searchParkingLots(query: String): Single> =
- ParkingLotDatabase.instance.getParkingDao()
- .getAllByQuery(SimpleSQLiteQuery(query))
- .subscribeOn(Schedulers.io())
-
- fun getHistory(): Single> =
- ParkingLotDatabase.instance.getHistoryDao()
- .getHistoryList()
- .subscribeOn(Schedulers.io())
-
- fun addHistory(id: String): Completable =
- ParkingLotDatabase.instance.getHistoryDao()
- .insert(History(id))
- .subscribeOn(Schedulers.io())
-
- fun deleteHistory(id: String): Completable =
- ParkingLotDatabase.instance.getHistoryDao()
- .deleteByID(id)
- .subscribeOn(Schedulers.io())
-
- fun getFavorites(): Single> =
- ParkingLotDatabase.instance.getFavoriteDao()
- .getLoveList()
- .subscribeOn(Schedulers.io())
-
- fun getFavorites(id: String): Single =
- ParkingLotDatabase.instance.getFavoriteDao()
- .getByID(id)
- .subscribeOn(Schedulers.io())
-
- fun addFavorite(id: String): Completable =
- ParkingLotDatabase.instance.getFavoriteDao()
- .insert(Favorite(id))
- .subscribeOn(Schedulers.io())
-
- fun deleteFavorite(id: String): Completable =
- ParkingLotDatabase.instance.getFavoriteDao()
- .deleteByID(id)
- .subscribeOn(Schedulers.io())
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt
deleted file mode 100644
index ddd29b6..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/Event.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.a1573595.parkinglotdemo.tool
-
-import androidx.lifecycle.Observer
-
-/**
- * Used as a wrapper for data that is exposed via a LiveData that represents an event.
- */
-open class Event(private val content: T) {
-
- @Suppress("MemberVisibilityCanBePrivate")
- var hasBeenHandled = false
- private set // Allow external read but not write
-
- /**
- * Returns the content and prevents its use again.
- */
- fun getContentIfNotHandled(): T? {
- return if (hasBeenHandled) {
- null
- } else {
- hasBeenHandled = true
- content
- }
- }
-
- /**
- * Returns the content, even if it's already been handled.
- */
- fun peekContent(): T = content
-}
-
-/**
- * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
- * already been handled.
- *
- * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
- */
-class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> {
- override fun onChanged(value: Event) {
- value.getContentIfNotHandled()?.let {
- onEventUnhandledContent(it)
- }
- }
-}
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt
deleted file mode 100644
index 509a90a..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/LatLngCoding.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package com.a1573595.parkinglotdemo.tool
-
-import kotlin.math.cos
-import kotlin.math.pow
-import kotlin.math.sin
-import kotlin.math.tan
-
-class LatLngCoding {
- companion object {
- private const val a = 6378137.0
- private const val b = 6356752.314245
- private const val lon0 = 121 * Math.PI / 180
- private const val k0 = 0.9999
- private const val dx = 250000
-
- fun calTWD97ToLonLat(x: Double, y: Double): String {
- var x = x
- var y = y
- val dy = 0.0
- val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5)
- x -= dx.toDouble()
- y -= dy
-
- // Calculate the Meridional Arc
- val M = y / k0
-
- // Calculate Footprint Latitude
- val mu =
- M / (a * (1.0 - e.pow(2.0) / 4.0 - 3 * e.pow(4.0) / 64.0 - 5 * e.pow(6.0) / 256.0))
- val e1 = (1.0 - (1.0 - e.pow(2.0)).pow(0.5)) / (1.0 + (1.0 - e.pow(2.0)).pow(0.5))
- val j1 = 3 * e1 / 2 - 27 * e1.pow(3.0) / 32.0
- val j2 = 21 * e1.pow(2.0) / 16 - 55 * e1.pow(4.0) / 32.0
- val j3 = 151 * e1.pow(3.0) / 96.0
- val j4 = 1097 * e1.pow(4.0) / 512.0
- val fp =
- mu + j1 * sin(2 * mu) + j2 * sin(4 * mu) + j3 * sin(6 * mu) + j4 * sin(
- 8 * mu
- )
-
- // Calculate Latitude and Longitude
- val e2 = (e * a / b).pow(2.0)
- val c1 = (e2 * cos(fp)).pow(2.0)
- val t1 = tan(fp).pow(2.0)
- val r1 = a * (1 - e.pow(2.0)) / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(3.0 / 2.0)
- val n1 = a / (1 - e.pow(2.0) * sin(fp).pow(2.0)).pow(0.5)
- val D = x / (n1 * k0)
-
- // 計算緯度
- val q1 = n1 * tan(fp) / r1
- val q2 = D.pow(2.0) / 2.0
- val q3 =
- (5 + 3 * t1 + 10 * c1 - 4 * c1.pow(2.0) - 9 * e2) * D.pow(4.0) / 24.0
- val q4 =
- (61 + 90 * t1 + 298 * c1 + 45 * t1.pow(2.0) - 3 * c1.pow(2.0) - 252 * e2) * D.pow(
- 6.0
- ) / 720.0
- var lat = fp - q1 * (q2 - q3 + q4)
-
- // 計算經度
- val q6 = (1 + 2 * t1 + c1) * D.pow(3.0) / 6
- val q7 =
- (5 - 2 * c1 + 28 * t1 - 3 * c1.pow(2.0) + 8 * e2 + 24 * t1.pow(2.0)) * D.pow(5.0) / 120.0
- var lon = lon0 + (D - q6 + q7) / cos(fp)
- lat = lat * 180 / Math.PI //緯
- lon = lon * 180 / Math.PI //經
- return "$lat,$lon"
- }
- }
-
- // 給WGS84經緯度度分秒轉成TWD97坐標
- fun lonLatToTWD97(lonD: Int, lonM: Int, lonS: Int, latD: Int, latM: Int, latS: Int): String {
- val radianLon = lonD.toDouble() + lonM.toDouble() / 60 + lonS.toDouble() / 3600
- val radianLat = latD.toDouble() + latM.toDouble() / 60 + latS.toDouble() / 3600
- return calLonLatToTWD97(radianLon, radianLat)
- }
-
- // 給WGS84經緯度弧度轉成TWD97坐標
- fun lonLatToWED97(radianLon: Double, radianLat: Double): String {
- return calLonLatToTWD97(radianLon, radianLat)
- }
-
- // 給TWD97坐標 轉成 WGS84 度分秒字串 (type1傳度分秒 2傳弧度)
- fun twd97ToLonLat(x: Double, y: Double, type: Int): String {
- var lonLat = ""
- if (type == 1) {
- val answer = calTWD97ToLonLat(x, y).split(",".toRegex()).toTypedArray()
- val lonDValue = answer[0].toInt()
- val lonMValue = ((answer[0].toDouble() - lonDValue) * 60).toInt()
- val lonSValue = (((answer[0].toDouble() - lonDValue) * 60 - lonMValue) * 60).toInt()
- val latDValue = answer[1].toInt()
- val latMValue = ((answer[1].toDouble() - latDValue) * 60).toInt()
- val latSValue = (((answer[1].toDouble() - latDValue) * 60 - latMValue) * 60).toInt()
- lonLat =
- lonDValue.toString() + "度" + lonMValue + "分" + lonSValue + "秒," + latDValue + "度" + latMValue + "分" + latSValue + "秒,"
- } else if (type == 2) {
- lonLat = calTWD97ToLonLat(x, y)
- }
- return lonLat
- }
-
- private fun calLonLatToTWD97(lon: Double, lat: Double): String {
- val lon = lon / 180 * Math.PI
- val lat = lat / 180 * Math.PI
-
- //---------------------------------------------------------
- val e = (1 - b.pow(2.0) / a.pow(2.0)).pow(0.5)
- val e2 = e.pow(2.0) / (1 - e.pow(2.0))
- val n = (a - b) / (a + b)
- val nu = a / (1 - e.pow(2.0) * sin(lat).pow(2.0)).pow(0.5)
- val p = lon - lon0
- val A =
- a * (1 - n + 5 / 4 * (n.pow(2.0) - n.pow(3.0)) + 81 / 64 * (n.pow(4.0) - n.pow(5.0)))
- val B =
- 3 * a * n / 2.0 * (1 - n + 7 / 8.0 * (n.pow(2.0) - n.pow(3.0)) + 55 / 64.0 * (n.pow(4.0) - n.pow(
- 5.0
- )))
- val C = 15 * a * n.pow(2.0) / 16.0 * (1 - n + 3 / 4.0 * (n.pow(2.0) - n.pow(3.0)))
- val D =
- 35 * a * n.pow(3.0) / 48.0 * (1 - n + 11 / 16.0 * (n.pow(2.0) - n.pow(3.0)))
- val E = 315 * a * n.pow(4.0) / 51.0 * (1 - n)
- val S =
- A * lat - B * sin(2 * lat) + C * sin(4 * lat) - D * sin(6 * lat) + E * sin(
- 8 * lat
- )
-
- //計算Y值
- val k1 = S * k0
- val k2 = k0 * nu * sin(2 * lat) / 4.0
- val k3 =
- k0 * nu * sin(lat) * cos(lat).pow(3.0) / 24.0 * (5 - tan(lat).pow(2.0) + 9 * e2 * cos(
- lat
- ).pow(2.0) + 4 * e2.pow(2.0) * cos(lat).pow(4.0))
- val y = k1 + k2 * p.pow(2.0) + k3 * p.pow(4.0)
-
- //計算X值
- val k4 = k0 * nu * cos(lat)
- val k5 =
- k0 * nu * cos(lat).pow(3.0) / 6.0 * (1 - tan(lat).pow(2.0) + e2 * cos(lat).pow(2.0))
- val x = k4 * p + k5 * p.pow(3.0) + dx
- return "$x,$y"
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt b/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt
deleted file mode 100644
index c71a821..0000000
--- a/app/src/main/java/com/a1573595/parkinglotdemo/tool/ParkingCluster.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.a1573595.parkinglotdemo.tool
-
-import com.google.android.gms.maps.model.LatLng
-import com.google.maps.android.clustering.ClusterItem
-
-class ParkingCluster(
- private val mPosition: LatLng,
- val id: String,
- val name: String?,
- val area: String?,
- val totalCar: Int,
- val totalMotor: Int,
- val totalBike: Int,
- val totalBus: Int
-) : ClusterItem {
- override fun getPosition(): LatLng = mPosition
-
- override fun getTitle(): String? = null
-
- override fun getSnippet(): String? = null
-
- override fun getZIndex(): Float? = null
-}
\ No newline at end of file
diff --git a/app/src/main/res/anim/grow_fade_in_from_bottom.xml b/app/src/main/res/anim/grow_fade_in_from_bottom.xml
deleted file mode 100644
index 0fe9963..0000000
--- a/app/src/main/res/anim/grow_fade_in_from_bottom.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bike.xml b/app/src/main/res/drawable/ic_bike.xml
deleted file mode 100644
index ec9c249..0000000
--- a/app/src/main/res/drawable/ic_bike.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bus.xml b/app/src/main/res/drawable/ic_bus.xml
deleted file mode 100644
index 74edece..0000000
--- a/app/src/main/res/drawable/ic_bus.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_car.xml b/app/src/main/res/drawable/ic_car.xml
deleted file mode 100644
index dbeb88a..0000000
--- a/app/src/main/res/drawable/ic_car.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_coin.xml b/app/src/main/res/drawable/ic_coin.xml
deleted file mode 100644
index ce7b47a..0000000
--- a/app/src/main/res/drawable/ic_coin.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
deleted file mode 100644
index 5148ba5..0000000
--- a/app/src/main/res/drawable/ic_delete.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
deleted file mode 100644
index 389bb28..0000000
--- a/app/src/main/res/drawable/ic_info.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml
rename to app/src/main/res/drawable/ic_launcher_foreground.xml
diff --git a/app/src/main/res/drawable/ic_motor.xml b/app/src/main/res/drawable/ic_motor.xml
deleted file mode 100644
index 6fb0e64..0000000
--- a/app/src/main/res/drawable/ic_motor.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml
deleted file mode 100644
index c75fe2b..0000000
--- a/app/src/main/res/drawable/ic_phone.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml
deleted file mode 100644
index 627a8c6..0000000
--- a/app/src/main/res/drawable/ic_pin.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_unfavorite.xml b/app/src/main/res/drawable/ic_unfavorite.xml
deleted file mode 100644
index 8a5a2fd..0000000
--- a/app/src/main/res/drawable/ic_unfavorite.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/rectangle_black.xml b/app/src/main/res/drawable/rectangle_black.xml
deleted file mode 100644
index 60ba4d1..0000000
--- a/app/src/main/res/drawable/rectangle_black.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rectangle_white.xml b/app/src/main/res/drawable/rectangle_white.xml
deleted file mode 100644
index 5df2dba..0000000
--- a/app/src/main/res/drawable/rectangle_white.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_detail.xml b/app/src/main/res/layout/activity_detail.xml
deleted file mode 100644
index a0e4bfe..0000000
--- a/app/src/main/res/layout/activity_detail.xml
+++ /dev/null
@@ -1,254 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_fuzzy_search.xml b/app/src/main/res/layout/activity_fuzzy_search.xml
deleted file mode 100644
index b5ace8d..0000000
--- a/app/src/main/res/layout/activity_fuzzy_search.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml
deleted file mode 100644
index 0bc7737..0000000
--- a/app/src/main/res/layout/activity_history.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 1580b9e..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml
deleted file mode 100644
index 18dc9de..0000000
--- a/app/src/main/res/layout/activity_map.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_parking_lot.xml b/app/src/main/res/layout/dialog_parking_lot.xml
deleted file mode 100644
index 89306f8..0000000
--- a/app/src/main/res/layout/dialog_parking_lot.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_parking_lot.xml b/app/src/main/res/layout/item_parking_lot.xml
deleted file mode 100644
index 51ef74d..0000000
--- a/app/src/main/res/layout/item_parking_lot.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_update.xml b/app/src/main/res/menu/menu_update.xml
deleted file mode 100644
index 2bc5d61..0000000
--- a/app/src/main/res/menu/menu_update.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-xxxhdpi/park.png b/app/src/main/res/mipmap-xxxhdpi/park.png
deleted file mode 100644
index 77559ea..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/park.png and /dev/null differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
deleted file mode 100644
index d5f4242..0000000
--- a/app/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
deleted file mode 100644
index 933b92e..0000000
--- a/app/src/main/res/values-zh/strings.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- 地圖模式
- 列表模式
- 最愛模式
- 歷史模式
-
- 資料下載中\u002e\u002e\u002e
- 資料處理中\u002e\u002e\u002e
-
- 總收錄%d筆資料
- 下載於: %s
-
- 公車:%d\t\t轎車:%d\t\t機車:%d\t\t自行車:%d
-
- 公車
- 轎車
- 機車
- 自行車
- 停車場名稱或地址
-
- 復原
- 刪除
-
- 再按一次退出
-
- 請輸入地址
-
\ No newline at end of file
diff --git a/app/src/main/res/values/diemns.xml b/app/src/main/res/values/diemns.xml
deleted file mode 100644
index 45361ef..0000000
--- a/app/src/main/res/values/diemns.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- 14dp
- 18dp
- 22dp
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 251f7f7..b018a09 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,29 +1,20 @@
- ParkingLotDemo
-
- Map mode
- List mode
- Favorite mode
- History mode
-
- Downloading\u002e\u002e\u002e
- Processing\u002e\u002e\u002e
-
- Total of %d data sets
- download at: %s
-
- Bus:%d\t\tCar:%d\t\tMoto:%d\t\tBike:%d
-
+ ParkingLot
+ Press again to exit
+ ParkingLot
+ No More
+ Favorite
+ History
+ Search
+ Map
+ Total of %1$s parking lots,\n download at: %2$s.
+ Keyword
Bus
Car
- Moto
+ Motor
Bike
- Parking name or address
-
- Recover
- Delete
-
- Press again to exit
-
- Enter address
+ Bus: %1$s
+ Car: %1$s
+ Motor: %1$s
+ Bike: %1$s
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index fffbff5..7ac90f6 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,16 +1,5 @@
+
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/ExampleUnitTest.kt b/app/src/test/java/com/a1573595/parkingdemo/ExampleUnitTest.kt
similarity index 89%
rename from app/src/test/java/com/a1573595/parkinglotdemo/ExampleUnitTest.kt
rename to app/src/test/java/com/a1573595/parkingdemo/ExampleUnitTest.kt
index aeace4d..911df1d 100644
--- a/app/src/test/java/com/a1573595/parkinglotdemo/ExampleUnitTest.kt
+++ b/app/src/test/java/com/a1573595/parkingdemo/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.a1573595.parkinglotdemo
+package com.a1573595.parkingdemo
import org.junit.Test
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/DetailViewModelTest.kt b/app/src/test/java/com/a1573595/parkinglotdemo/DetailViewModelTest.kt
deleted file mode 100644
index b7f5f90..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/DetailViewModelTest.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.a1573595.parkinglotdemo.database.Favorite
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotDataStore
-import com.a1573595.parkinglotdemo.page.detail.DetailViewModel
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.mockk
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.lang.Exception
-
-@RunWith(AndroidJUnit4::class)
-class DetailViewModelTest {
- @get:Rule
- val rule = InstantTaskExecutorRule()
-
- @get:Rule
- val rxSchedulerRule = RxSchedulerRule()
-
- private val context: Application = ApplicationProvider.getApplicationContext()
-
- private lateinit var viewModel: DetailViewModel
- private val repository: ParkingLotRepository = mockk(relaxed = true)
-
- private val parkingLot = ParkingLot("001", "", "A", "", "", "", "")
-
- @Before
- fun setup() {
- MockKAnnotations.init(this)
-
- ParkingLotDataStore.build(context)
-
- viewModel = DetailViewModel(context, repository)
- }
-
- @Test
- fun load_favorite_data() {
- val id = "001"
- every { repository.getFavorites(any()) }.returns(Single.just(Favorite(id)))
- every { repository.getParkingLot(any()) }.returns(Single.just(parkingLot))
- every { repository.addHistory(any()) }.returns(Completable.complete())
-
- viewModel.loadData(id)
-
- Assert.assertEquals(
- true,
- viewModel.isFavoriteEvent.value?.peekContent()
- )
-
- Assert.assertEquals(
- parkingLot,
- viewModel.parkingLotEvent.value?.peekContent()
- )
- }
-
- @Test
- fun load_unfavorite_data() {
- val id = "002"
- every { repository.getFavorites(any()) }.returns(Single.error(Exception()))
- every { repository.getParkingLot(any()) }.returns(Single.just(parkingLot))
- every { repository.addHistory(any()) }.returns(Completable.complete())
-
- viewModel.loadData(id)
-
- Assert.assertEquals(
- false,
- viewModel.isFavoriteEvent.value?.peekContent()
- )
-
- Assert.assertEquals(
- parkingLot,
- viewModel.parkingLotEvent.value?.peekContent()
- )
- }
-
- @Test
- fun addFavorite() {
- every { repository.deleteFavorite(any()) }.returns(Completable.complete())
- every { repository.addFavorite(any()) }.returns(Completable.complete())
-
- viewModel.addFavorite("001")
-
- Assert.assertEquals(
- true,
- viewModel.isFavoriteEvent.value?.peekContent()
- )
-
- viewModel.addFavorite("001")
-
- Assert.assertEquals(
- false,
- viewModel.isFavoriteEvent.value?.peekContent()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/FuzzySearchViewModelTest.kt b/app/src/test/java/com/a1573595/parkinglotdemo/FuzzySearchViewModelTest.kt
deleted file mode 100644
index c9bb073..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/FuzzySearchViewModelTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotDataStore
-import com.a1573595.parkinglotdemo.page.fuzzySearch.FuzzySearchViewModel
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.mockk
-import io.reactivex.rxjava3.core.Single
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FuzzySearchViewModelTest {
- @get:Rule
- val rule = InstantTaskExecutorRule()
-
- @get:Rule
- val rxSchedulerRule = RxSchedulerRule()
-
- private val context: Application = ApplicationProvider.getApplicationContext()
-
- private lateinit var viewModel: FuzzySearchViewModel
- private val repository: ParkingLotRepository = mockk(relaxed = true)
-
- private val parkingLots = listOf(
- ParkingLot("001", "", "A", "", "", "", ""),
- ParkingLot("002", "", "B", "", "", "", ""),
- ParkingLot("003", "", "C", "", "", "", ""),
- ParkingLot("004", "", "D", "", "", "", ""),
- ParkingLot("005", "", "E", "", "", "", ""),
- ParkingLot("006", "", "F", "", "", "", ""),
- )
-
- @Before
- fun setup() {
- MockKAnnotations.init(this)
-
- ParkingLotDataStore.build(context)
-// ParkingLotDatabase.build(context)
-
- viewModel = FuzzySearchViewModel(context, repository)
-
- every { repository.searchParkingLots(any()) }.returns(Single.just(parkingLots))
- }
-
- @Test
- fun loadDataSet() {
- viewModel.loadDataSet()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun setKeyword() {
- every { repository.searchParkingLots(any()) }.returns(Single.just(parkingLots))
-
- viewModel.setKeyword("")
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/HistoryModelTest.kt b/app/src/test/java/com/a1573595/parkinglotdemo/HistoryModelTest.kt
deleted file mode 100644
index 3326746..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/HistoryModelTest.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotDataStore
-import com.a1573595.parkinglotdemo.page.history.HistoryActivity.Companion.MODE_FAVORITE
-import com.a1573595.parkinglotdemo.page.history.HistoryActivity.Companion.MODE_HISTORY
-import com.a1573595.parkinglotdemo.page.history.HistoryModel
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.mockk
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Single
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class HistoryModelTest {
- @get:Rule
- val rule = InstantTaskExecutorRule()
-
- @get:Rule
- val rxSchedulerRule = RxSchedulerRule()
-
- private val context: Application = ApplicationProvider.getApplicationContext()
-
- private lateinit var viewModel: HistoryModel
- private val repository: ParkingLotRepository = mockk(relaxed = true)
-
- private val parkingLots = listOf(
- ParkingLot("001", "", "A", "", "", "", ""),
- ParkingLot("002", "", "B", "", "", "", ""),
- ParkingLot("003", "", "C", "", "", "", ""),
- ParkingLot("004", "", "D", "", "", "", ""),
- ParkingLot("005", "", "E", "", "", "", ""),
- ParkingLot("006", "", "F", "", "", "", ""),
- )
-
- @Before
- fun setup() {
- MockKAnnotations.init(this)
-
- ParkingLotDataStore.build(context)
-// ParkingLotDatabase.build(context)
-
- viewModel = HistoryModel(context, repository)
-
- every { repository.getFavorites() }.returns(Single.just(parkingLots))
- every { repository.getHistory() }.returns(Single.just(parkingLots))
- }
-
- @Test
- fun load_favorite_dataset() {
- viewModel.setMode(MODE_FAVORITE)
- viewModel.loadDataSet()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun load_history_dataset() {
- viewModel.setMode(MODE_HISTORY)
- viewModel.loadDataSet()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun delete_favorite() {
- viewModel.setMode(MODE_FAVORITE)
- viewModel.loadDataSet()
-
- every { repository.deleteFavorite(any()) }.returns(Completable.complete())
-
- viewModel.delete(1)
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun delete_history() {
- viewModel.setMode(MODE_HISTORY)
- viewModel.loadDataSet()
-
- every { repository.deleteHistory(any()) }.returns(Completable.complete())
-
- viewModel.delete(1)
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun undo_delete_favorite() {
- viewModel.setMode(MODE_FAVORITE)
- viewModel.loadDataSet()
-
- every { repository.deleteFavorite(any()) }.returns(Completable.complete())
-
- viewModel.delete(0)
- viewModel.undoDelete()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun undo_delete_history() {
- viewModel.setMode(MODE_HISTORY)
- viewModel.loadDataSet()
-
- every { repository.deleteHistory(any()) }.returns(Completable.complete())
-
- viewModel.delete(0)
- viewModel.undoDelete()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/MainViewModelTest.kt b/app/src/test/java/com/a1573595/parkinglotdemo/MainViewModelTest.kt
deleted file mode 100644
index a665b6b..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/MainViewModelTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.page.main.MainViewModel
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.mockk
-import io.reactivex.rxjava3.core.Completable
-import io.reactivex.rxjava3.core.Flowable
-import io.reactivex.rxjava3.core.Single
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Remove android:name=".MainApplication" in AndroidManifest for test
- * android-sqlcipher-database only run on Android instead of JVM.
- * https://github.com/sqlcipher/android-database-sqlcipher/issues/105
- */
-@RunWith(AndroidJUnit4::class)
-class MainViewModelTest {
- @get:Rule
- val rule = InstantTaskExecutorRule()
-
- @get:Rule
- val rxSchedulerRule = RxSchedulerRule()
-
- private val context: Application = ApplicationProvider.getApplicationContext()
-
- private lateinit var viewModel: MainViewModel
- private val repository: ParkingLotRepository = mockk(relaxed = true)
-
- @Before
- fun setup() {
- MockKAnnotations.init(this)
-
- viewModel = MainViewModel(context, repository)
- }
-
- @Test
- fun load_data_set() {
- every { repository.getUpdateTime() }.returns(Flowable.just(1000L))
- every { repository.getParkingLots() }.returns(Single.just(emptyList()))
-
- viewModel.loadDataSet()
-
- Assert.assertNotNull(
- viewModel.updateTimeEvent.value
- )
-
- Assert.assertEquals(
- emptyList(),
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-
- @Test
- fun updateDataSet() {
- every { repository.downloadDataSet() }.returns(Single.just(emptyList()))
- every { repository.deleteDataSet() }.returns(Completable.complete())
- every { repository.writeDataSet(any()) }.returns(Single.just(emptyList()))
- every { repository.getUpdateTime() }.returns(Flowable.just(1000L))
- every { repository.getParkingLots() }.returns(Single.just(emptyList()))
-
- viewModel.updateDataSet()
-
- Assert.assertNotNull(
- viewModel.updateTimeEvent.value
- )
-
- Assert.assertEquals(
- emptyList(),
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/MapViewModelTest.kt b/app/src/test/java/com/a1573595/parkinglotdemo/MapViewModelTest.kt
deleted file mode 100644
index 895b4de..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/MapViewModelTest.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import android.app.Application
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.a1573595.parkinglotdemo.database.ParkingLot
-import com.a1573595.parkinglotdemo.database.ParkingLotDataStore
-import com.a1573595.parkinglotdemo.page.map.MapViewModel
-import com.a1573595.parkinglotdemo.repository.ParkingLotRepository
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.mockk
-import io.reactivex.rxjava3.core.Single
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class MapViewModelTest {
- @get:Rule
- val rule = InstantTaskExecutorRule()
-
- @get:Rule
- val rxSchedulerRule = RxSchedulerRule()
-
- private val context: Application = ApplicationProvider.getApplicationContext()
-
- private lateinit var viewModel: MapViewModel
- private val repository: ParkingLotRepository = mockk(relaxed = true)
-
- private val parkingLots = listOf(
- ParkingLot("001", "", "A", "", "", "", ""),
- ParkingLot("002", "", "B", "", "", "", ""),
- ParkingLot("003", "", "C", "", "", "", ""),
- ParkingLot("004", "", "D", "", "", "", ""),
- ParkingLot("005", "", "E", "", "", "", ""),
- ParkingLot("006", "", "F", "", "", "", ""),
- )
-
- @Before
- fun setup() {
- MockKAnnotations.init(this)
-
- ParkingLotDataStore.build(context)
-
- viewModel = MapViewModel(context, repository)
- }
-
- @Test
- fun loadDataSet() {
- every { repository.getParkingLots() } returns Single.just(parkingLots)
-
- viewModel.loadDataSet()
-
- Assert.assertEquals(
- parkingLots,
- viewModel.dataSetEvent.value?.peekContent()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/a1573595/parkinglotdemo/RxSchedulerRule.kt b/app/src/test/java/com/a1573595/parkinglotdemo/RxSchedulerRule.kt
deleted file mode 100644
index c040bb9..0000000
--- a/app/src/test/java/com/a1573595/parkinglotdemo/RxSchedulerRule.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.a1573595.parkinglotdemo
-
-import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
-import io.reactivex.rxjava3.core.Scheduler
-import io.reactivex.rxjava3.disposables.Disposable
-import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler
-import io.reactivex.rxjava3.plugins.RxJavaPlugins
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-import java.util.concurrent.TimeUnit
-
-class RxSchedulerRule : TestRule {
- private val immediate: Scheduler = object : Scheduler() {
- override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable {
- // this prevents StackOverflowErrors when scheduling with a delay
- return super.scheduleDirect(run, 0, unit)
- }
-
- override fun createWorker(): Worker {
- return ExecutorScheduler.ExecutorWorker({ obj: Runnable -> obj.run() }, true, false)
- }
- }
-
- override fun apply(base: Statement, description: Description): Statement {
- return object : Statement() {
- @Throws(Throwable::class)
- override fun evaluate() {
- RxJavaPlugins.setComputationSchedulerHandler { immediate }
- RxJavaPlugins.setInitIoSchedulerHandler { immediate }
- RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
- RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
- RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
- RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
-
- try {
- base.evaluate()
- } finally {
- RxJavaPlugins.reset()
- RxAndroidPlugins.reset()
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 7978ea3..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-plugins {
- id 'com.android.application' version '8.3.0' apply false
- id 'com.android.library' version '8.3.0' apply false
- id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
- id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..cbee3ef
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,13 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ dependencies {
+ classpath(libs.secrets.gradle.plugin)
+ }
+}
+
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.google.dagger.hilt.android) apply false
+ alias(libs.plugins.jetbrains.android) apply false
+ alias(libs.plugins.devtools.ksp) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 996855f..20e2a01 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,18 +8,16 @@
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app"s APK
+# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
-android.defaults.buildfeatures.buildconfig=true
-android.nonTransitiveRClass=true
-android.nonFinalResIds=false
-org.gradle.configuration-cache=true
\ No newline at end of file
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..b6b6e45
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,84 @@
+[versions]
+activityCompose = "1.9.1"
+agp = "8.5.2"
+composeBom = "2024.08.00"
+coreKtx = "1.13.1"
+coreTesting = "2.2.0"
+datastorePreferences = "1.1.1"
+espressoCore = "3.6.1"
+hiltAndroid = "2.52"
+hiltCompiler = "2.52"
+hiltNavigationCompose = "1.2.0"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+kotlin = "2.0.0"
+kotlinxCoroutinesTest = "1.8.1"
+kotlinxSerializationJson = "1.7.2"
+ksp = "2.0.10-1.0.24"
+leakcanaryAndroid = "2.14"
+lifecycleRuntimeKtx = "2.8.4"
+loggingInterceptor = "4.12.0"
+mapsCompose = "6.1.1"
+mapsComposeUtils = "6.1.1"
+materialIconsExtended = "1.6.8"
+mockk = "1.13.12"
+navigationCompose = "2.7.7"
+navigationTesting = "2.7.7"
+pagingRuntimeKtx = "3.3.2"
+retrofit = "2.11.0"
+retrofit2KotlinxSerializationConverter = "1.0.0"
+roomCompiler = "2.6.1"
+roomKtx = "2.6.1"
+roomPaging = "2.6.1"
+roomRuntime = "2.6.1"
+secretsGradlePlugin = "2.0.1"
+turbine = "1.1.0"
+
+[libraries]
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
+androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingRuntimeKtx" }
+androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
+androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomPaging" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigationTesting" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
+hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
+logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
+maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "mapsComposeUtils" }
+maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }
+mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
+retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
+room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
+secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
+turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
+jetbrains-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 13372ae..e708b1c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2cc372b..d856f25 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Jan 31 10:15:43 CST 2022
+#Fri Aug 23 10:41:28 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
\ No newline at end of file
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
index 9d82f78..4f906e0
--- a/gradlew
+++ b/gradlew
@@ -1,4 +1,20 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
##
@@ -6,20 +22,38 @@
##
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
-warn ( ) {
+warn () {
echo "$*"
}
-die ( ) {
+die () {
echo
echo "$*"
echo
@@ -30,6 +64,7 @@ die ( ) {
cygwin=false
msys=false
darwin=false
+nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
@@ -40,28 +75,14 @@ case "`uname`" in
MINGW* )
msys=true
;;
+ NONSTOP* )
+ nonstop=true
+ ;;
esac
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -85,7 +106,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -105,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -134,27 +156,30 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 8a0b282..ac1b06f 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,90 +1,89 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/local.defaults.properties b/local.defaults.properties
new file mode 100644
index 0000000..e56e0bf
--- /dev/null
+++ b/local.defaults.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+MAPS_API_KEY=YOUR_API_KEY
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 1dd77f6..0000000
--- a/settings.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-pluginManagement {
- repositories {
- gradlePluginPortal()
- google()
- mavenCentral()
- }
-}
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-rootProject.name='ParkingLotDemo'
-include ':app'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..f9f7e9d
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "ParkingLot"
+include(":app")
+
\ No newline at end of file