diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index cc893a2..44545b0 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -1,22 +1,24 @@
name: Build android
-on: [push]
+on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-java@v1
- with:
- java-version: 11
- - name: Make Gradle executable
- run: chmod +x ./gradlew
- - name: Kotlin Lint with Gradle
- run: ./gradlew ktlint
- - name: Kotlin static code analysis with Gradle
- run: ./gradlew detekt
- - name: Test Debug APK
- run: ./gradlew test
- - name: Build with Gradle
- run: ./gradlew build
- - name: Build Debug APK
- run: ./gradlew assembleDebug
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ - name: Make Gradle executable
+ run: chmod +x ./gradlew
+ - name: Kotlin Lint with Gradle
+ run: |
+ echo "MAPS_API_KEY=${{ secrets.MAPS_API_KEY }}" > local.properties
+ ./gradlew ktlint
+ - name: Kotlin static code analysis with Gradle
+ run: ./gradlew detekt
+ - name: Test Debug APK
+ run: ./gradlew test
+ - name: Build with Gradle
+ run: ./gradlew build
+ - name: Build Debug APK
+ run: ./gradlew assembleDebug
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 0380d8d..958a5be 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index ad3859b..3ee8039 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,9 +2,13 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id "io.gitlab.arturbosch.detekt" version "1.16.0"
+ id 'kotlin-kapt'
+ id 'dagger.hilt.android.plugin'
+ id 'com.google.secrets_gradle_plugin' version '0.6'
}
apply plugin: "io.gitlab.arturbosch.detekt"
+apply plugin: 'dagger.hilt.android.plugin'
android {
compileSdkVersion 30
@@ -17,7 +21,16 @@ android {
versionCode 1
versionName "21.1.0"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner "com.deo.compose.TestRunner"
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += [
+ "room.schemaLocation" : "$projectDir/schemas".toString(),
+ "room.incremental" : "true",
+ "room.expandProjection": "true"
+ ]
+ }
+ }
}
buildTypes {
@@ -27,12 +40,34 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
+ packagingOptions {
+ // Multiple dependency bring these files in. Exclude them to enable
+ // our test APK to build (has no effect on our AARs)
+ excludes += "/META-INF/AL2.0"
+ excludes += "/META-INF/LGPL2.1"
+ }
+ // for spatialite
+ splits {
+ abi {
+ enable true
+ reset()
+ include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
+ }
+ }
+ signingConfigs {
+ debug {
+ storeFile rootProject.file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
}
dependencies {
@@ -42,14 +77,43 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+
+ // local unit test
testImplementation 'junit:junit:4.13.2'
+ testImplementation "com.google.truth:truth:1.1"
+ // instrumentation test
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ androidTestImplementation 'com.google.dagger:hilt-android-testing:2.31.2-alpha'
+ // androidx test
+ androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
+ // fluent assertions for Java and Android
+ androidTestImplementation "com.google.truth:truth:1.1"
// Detekt
detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.16.0"
// Memory leak detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+
+ // coroutines
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
+ // coroutine test
+ androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3"
+
+ // room - databases
+ implementation "androidx.room:room-runtime:2.3.0"
+ kapt "androidx.room:room-compiler:2.3.0"
+ implementation "androidx.room:room-ktx:2.3.0"
+ // spatialite
+ implementation "com.github.anboralabs:spatia-room:0.1.0"
+ testImplementation "androidx.room:room-testing:2.3.0"
+
+ // dagger hilt
+ implementation "com.google.dagger:hilt-android:2.31.2-alpha"
+ kapt "com.google.dagger:hilt-android-compiler:2.31.2-alpha"
+ // dagger-hilt test
+ kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.31.2-alpha'
}
// detekt
diff --git a/app/schemas/com.deo.sticky.di.StickyDatabase/1.json b/app/schemas/com.deo.sticky.di.StickyDatabase/1.json
new file mode 100644
index 0000000..e4309bd
--- /dev/null
+++ b/app/schemas/com.deo.sticky.di.StickyDatabase/1.json
@@ -0,0 +1,58 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "6b3f0130269f5981bad585e467ae02c3",
+ "entities": [
+ {
+ "tableName": "places",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `latitude` REAL, `longitude` REAL, `geometry` BLOB)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "geometry",
+ "columnName": "geometry",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b3f0130269f5981bad585e467ae02c3')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/deo/sticky/MainCoroutinesRule.kt b/app/src/androidTest/java/com/deo/sticky/MainCoroutinesRule.kt
new file mode 100644
index 0000000..4ef6916
--- /dev/null
+++ b/app/src/androidTest/java/com/deo/sticky/MainCoroutinesRule.kt
@@ -0,0 +1,29 @@
+package com.deo.sticky
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ko
+ */
+@ExperimentalCoroutinesApi
+class MainCoroutinesRule : TestRule, TestCoroutineScope by TestCoroutineScope() {
+
+ private val testCoroutinesDispatcher = TestCoroutineDispatcher()
+
+ override fun apply(base: Statement?, description: Description?) = object : Statement() {
+ override fun evaluate() {
+ Dispatchers.setMain(testCoroutinesDispatcher)
+ base?.evaluate()
+ cleanupTestCoroutines()
+ Dispatchers.resetMain()
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/deo/sticky/TestRunner.kt b/app/src/androidTest/java/com/deo/sticky/TestRunner.kt
new file mode 100644
index 0000000..e30b464
--- /dev/null
+++ b/app/src/androidTest/java/com/deo/sticky/TestRunner.kt
@@ -0,0 +1,18 @@
+package com.deo.compose
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class TestRunner : AndroidJUnitRunner() {
+ override fun newApplication(
+ cl: ClassLoader?,
+ name: String?,
+ context: Context?
+ ): Application {
+ return super.newApplication(
+ cl, HiltTestApplication::class.java.name, context
+ )
+ }
+}
diff --git a/app/src/androidTest/java/com/deo/sticky/di/TestDatabaseModule.kt b/app/src/androidTest/java/com/deo/sticky/di/TestDatabaseModule.kt
new file mode 100644
index 0000000..1e120fe
--- /dev/null
+++ b/app/src/androidTest/java/com/deo/sticky/di/TestDatabaseModule.kt
@@ -0,0 +1,40 @@
+package com.deo.sticky.di
+
+import android.content.Context
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteDatabase
+import co.anbora.labs.spatia.builder.SpatiaRoom
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import javax.inject.Singleton
+
+@TestInstallIn(
+ components = [SingletonComponent::class],
+ replaces = [DatabaseModule::class]
+)
+@Module
+class TestDatabaseModule {
+ /**
+ * 장소 데이터베이스
+ */
+ @Singleton
+ @Provides
+ fun provideSpatiaDatabase(
+ @ApplicationContext
+ context: Context
+ ) = SpatiaRoom.databaseBuilder(
+ context.applicationContext,
+ StickyDatabase::class.java,
+ "sticky-test.db"
+ ).addCallback(object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ super.onCreate(db)
+ }
+ }).fallbackToDestructiveMigration().build()
+// .setTransactionExecutor(
+// Executors.newSingleThreadExecutor()
+// ).build()
+}
diff --git a/app/src/androidTest/java/com/deo/sticky/features/map/models/PlaceDaoTest.kt b/app/src/androidTest/java/com/deo/sticky/features/map/models/PlaceDaoTest.kt
new file mode 100644
index 0000000..fe9a529
--- /dev/null
+++ b/app/src/androidTest/java/com/deo/sticky/features/map/models/PlaceDaoTest.kt
@@ -0,0 +1,56 @@
+package com.deo.sticky.features.map.models
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.filters.SmallTest
+import com.deo.sticky.MainCoroutinesRule
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+@HiltAndroidTest
+@ExperimentalCoroutinesApi
+class PlaceDaoTest {
+ @get:Rule
+ var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineRule = MainCoroutinesRule()
+
+ @Before
+ fun setup() {
+ hiltRule.inject()
+ }
+
+ @Inject
+ lateinit var placeDao: PlaceDao
+
+ @Test
+ fun `장소 저장하기`() = runBlockingTest {
+ assertThat(
+ placeDao.add("강남 12번 출구 커피빈", 37.5092672, 127.026935)
+ ).isAtLeast(1)
+ }
+
+ @Test
+ fun `반경 내 장소 리스트 불러오기`() = runBlockingTest {
+ assertThat(placeDao.add("강남 12번 출구 커피빈", 37.5092672, 127.026935))
+ .isAtLeast(1)
+ assertThat(placeDao.add("강남 11번 출구 할리스커피", 37.4985584, 127.0278279))
+ .isAtLeast(1)
+ assertThat(placeDao.add("강남 1번 출구 스타벅", 37.497721, 127.0269938))
+ .isAtLeast(1)
+ // 강남역
+ assertThat(placeDao.getPlacesWithinRadius(37.4977252, 127.0269938, radius = 1_000))
+ .hasSize(3)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a0c824f..9c13991 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,13 +2,23 @@
+
+
+
+
+
+
+ android:theme="@style/Theme.Sticky">
+
diff --git a/app/src/main/java/com/deo/sticky/MainActivity.kt b/app/src/main/java/com/deo/sticky/MainActivity.kt
index 2cecdd2..55a70e6 100644
--- a/app/src/main/java/com/deo/sticky/MainActivity.kt
+++ b/app/src/main/java/com/deo/sticky/MainActivity.kt
@@ -2,7 +2,9 @@ package com.deo.sticky
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
+import dagger.hilt.android.AndroidEntryPoint
+@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/java/com/deo/sticky/StickyApplication.kt b/app/src/main/java/com/deo/sticky/StickyApplication.kt
new file mode 100644
index 0000000..92db97b
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/StickyApplication.kt
@@ -0,0 +1,7 @@
+package com.deo.sticky
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class StickyApplication : Application()
diff --git a/app/src/main/java/com/deo/sticky/base/BaseDao.kt b/app/src/main/java/com/deo/sticky/base/BaseDao.kt
new file mode 100644
index 0000000..bcaa695
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/base/BaseDao.kt
@@ -0,0 +1,24 @@
+package com.deo.sticky.base
+
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Transaction
+
+interface BaseDao {
+ @Transaction
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upserts(vararg entities: T): List
+
+ @Transaction
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: T): Long
+
+ @Transaction
+ @Delete
+ suspend fun delete(entity: T)
+
+ @Transaction
+ @Delete
+ suspend fun deletes(entities: List)
+}
diff --git a/app/src/main/java/com/deo/sticky/di/DaoModule.kt b/app/src/main/java/com/deo/sticky/di/DaoModule.kt
new file mode 100644
index 0000000..f746c96
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/di/DaoModule.kt
@@ -0,0 +1,18 @@
+package com.deo.sticky.di
+
+import com.deo.sticky.features.map.models.PlaceDao
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DaoModule {
+ @Singleton
+ @Provides
+ fun providePlaceDao(
+ database: StickyDatabase
+ ): PlaceDao = database.getPlaceDao()
+}
diff --git a/app/src/main/java/com/deo/sticky/di/DatabaseModule.kt b/app/src/main/java/com/deo/sticky/di/DatabaseModule.kt
new file mode 100644
index 0000000..c56fffb
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/di/DatabaseModule.kt
@@ -0,0 +1,35 @@
+package com.deo.sticky.di
+
+import android.content.Context
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteDatabase
+import co.anbora.labs.spatia.builder.SpatiaRoom
+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)
+class DatabaseModule {
+ @Singleton
+ @Provides
+ fun provideDatabase(
+ @ApplicationContext
+ context: Context
+ ) = SpatiaRoom.databaseBuilder(
+ context,
+ StickyDatabase::class.java,
+ "sticky.db"
+ ).addCallback(object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ super.onCreate(db)
+ // add init query
+ }
+ }).fallbackToDestructiveMigration().build()
+// .setTransactionExecutor(
+// Executors.newSingleThreadExecutor()
+// ).build()
+}
diff --git a/app/src/main/java/com/deo/sticky/di/StickyDatabase.kt b/app/src/main/java/com/deo/sticky/di/StickyDatabase.kt
new file mode 100644
index 0000000..ef52b84
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/di/StickyDatabase.kt
@@ -0,0 +1,11 @@
+package com.deo.sticky.di
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.deo.sticky.features.map.models.PlaceDao
+import com.deo.sticky.features.map.models.entity.Place
+
+@Database(entities = [Place::class], version = 1)
+abstract class StickyDatabase : RoomDatabase() {
+ abstract fun getPlaceDao(): PlaceDao
+}
diff --git a/app/src/main/java/com/deo/sticky/features/map/models/Place.kt b/app/src/main/java/com/deo/sticky/features/map/models/Place.kt
new file mode 100644
index 0000000..2e39f66
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/features/map/models/Place.kt
@@ -0,0 +1,29 @@
+package com.deo.sticky.features.map.models.entity
+
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * 장소
+ *
+ * name: 장소명
+ * latitude: 위도
+ * longitude: 경도
+ * geometry: spatialite의 geometry
+ */
+@Entity(tableName = "places")
+data class Place(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long? = null,
+ val name: String? = null,
+ val latitude: Double? = null,
+ val longitude: Double? = null,
+ val geometry: ByteArray? = null
+)
+
+data class PlaceWithDistance(
+ @Embedded
+ val place: Place?,
+ val distance: Double?
+)
diff --git a/app/src/main/java/com/deo/sticky/features/map/models/PlaceDao.kt b/app/src/main/java/com/deo/sticky/features/map/models/PlaceDao.kt
new file mode 100644
index 0000000..e5125f3
--- /dev/null
+++ b/app/src/main/java/com/deo/sticky/features/map/models/PlaceDao.kt
@@ -0,0 +1,50 @@
+package com.deo.sticky.features.map.models
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.SkipQueryVerification
+import com.deo.sticky.base.BaseDao
+import com.deo.sticky.features.map.models.entity.Place
+import com.deo.sticky.features.map.models.entity.PlaceWithDistance
+
+@Dao
+interface PlaceDao : BaseDao {
+ /**
+ * 장소 추가하기 (+ 좌표 저장)
+ */
+ @Query(
+ """
+ INSERT INTO places (name, latitude, longitude, geometry)
+ VALUES (:name, :latitude, :longitude, MakePoint(:latitude, :longitude, 4326))
+ """
+ )
+ @SkipQueryVerification
+ suspend fun add(
+ name: String,
+ latitude: Double,
+ longitude: Double,
+ ): Long
+
+ /**
+ * 반경 내 장소 리스트 불러오기
+ */
+ @Query(
+ """
+ SELECT id,
+ name,
+ latitude,
+ longitude,
+ geometry,
+ GeodesicLength(ShortestLine(geometry, MakePoint(:latitude, :longitude, 4326))) as distance
+ FROM places
+ WHERE PtDistWithin(geometry, MakePoint(:latitude, :longitude, 4326), :radius)
+ ORDER BY distance ASC
+ """
+ )
+ @SkipQueryVerification
+ suspend fun getPlacesWithinRadius(
+ latitude: Double,
+ longitude: Double,
+ radius: Int
+ ): List
+}
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 58e252b..a55e6aa 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,6 +1,6 @@
-