diff --git a/.gitignore b/.gitignore index b4959436..8878fbe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +<<<<<<< HEAD +======= .DS_Store ### Android template +>>>>>>> origin/step0 # Gradle files .gradle/ build/ @@ -33,5 +36,11 @@ google-services.json # Android Profiling *.hprof +<<<<<<< HEAD + +# Mac OS +.DS_Store +======= /keyStore /app/release +>>>>>>> origin/step0 diff --git a/README.md b/README.md index 73ba54e1..44485c7d 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ # android-map-refactoring +# 기능 요구 사항 +1. 데이터베이스를 Room으로 변경한다. +2. 가능한 모든 부분에 대해서 의존성 주입을 적용한다. +# 프로그래밍 요구 사항 +1. 의존성 주입을 위해서 Hilt를 사용한다. +2. 코드 컨벤션을 준수하며 프로그래밍한다. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c1a15f2..ca60069f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,8 +8,8 @@ plugins { } android { - namespace = "campus.tech.kakao.map" compileSdk = 34 + namespace = "campus.tech.kakao.map" defaultConfig { applicationId = "campus.tech.kakao.map" @@ -17,6 +17,15 @@ android { targetSdk = 34 versionCode = 1 versionName = "1.0" + ndk { + abiFilters.add("arm64-v8a") + abiFilters.add("armeabi-v7a") + abiFilters.add("x86") + abiFilters.add("x86_64") + } + dataBinding { + enable = true + } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -26,55 +35,71 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", + "proguard-rules.pro" ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } buildFeatures { + viewBinding = true dataBinding = true buildConfig = true } } dependencies { - - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("com.google.android.material:material:1.12.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.recyclerview:recyclerview:1.3.2") - implementation("com.squareup.retrofit2:retrofit:2.11.0") - implementation("com.squareup.retrofit2:converter-gson:2.11.0") - implementation("com.kakao.maps.open:android:2.9.5") - implementation("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.test:core-ktx:1.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") + implementation("androidx.test.espresso:espresso-core:3.6.1") + testImplementation ("org.junit.jupiter:junit-jupiter-api:5.8.2") + testRuntimeOnly ("org.junit.jupiter:junit-jupiter-engine:5.8.2") + implementation("androidx.test.ext:junit-ktx:1.2.1") + implementation("androidx.test.espresso:espresso-intents:3.6.1") + testImplementation("io.mockk:mockk:1.13.11") + testImplementation("io.mockk:mockk-android:1.13.11") + testImplementation("io.mockk:mockk-agent:1.13.11") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:4.8.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") implementation("androidx.room:room-runtime:2.6.1") kapt("androidx.room:room-compiler:2.6.1") + testImplementation("androidx.room:room-testing:2.6.1") implementation("com.google.dagger:hilt-android:2.48.1") kapt("com.google.dagger:hilt-compiler:2.48.1") - implementation("androidx.activity:activity-ktx:1.9.0") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4") implementation("androidx.room:room-ktx:2.6.1") - testImplementation("androidx.room:room-testing:2.6.1") - testImplementation("junit:junit:4.13.2") + implementation("com.google.android.gms:play-services-maps:19.0.0") + implementation("com.google.android.libraries.places:places:3.5.0") testImplementation("io.mockk:mockk-android:1.13.11") testImplementation("io.mockk:mockk-agent:1.13.11") testImplementation("androidx.arch.core:core-testing:2.2.0") testImplementation("org.robolectric:robolectric:4.11.1") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") androidTestImplementation("androidx.test:rules:1.6.1") - androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1") + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("androidx.activity:activity:1.9.1") + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.google.ar.sceneform:core:1.17.1") + implementation("com.google.android.gms:play-services-location:21.3.0") + implementation("androidx.multidex:multidex:2.0.1") + implementation("com.kakao.maps.open:android:2.9.5") + implementation("com.kakao.sdk:v2-all:2.20.3") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.2") } diff --git a/app/src/androidTest/java/campus/tech/kakao/map/ErrorActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/ErrorActivityTest.kt new file mode 100644 index 00000000..8d72b9ff --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/ErrorActivityTest.kt @@ -0,0 +1,49 @@ +package campus.tech.kakao.map + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.runner.AndroidJUnit4 +import campus.tech.kakao.map.View.ErrorActivity +import campus.tech.kakao.map.View.Map_Activity +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ErrorActivityTest { + + private lateinit var scenario: ActivityScenario + + @Before + fun setup() { + scenario = ActivityScenario.launch(ErrorActivity::class.java) + } + + @After + fun tearDown() { + scenario.close() + } + + @Test + fun testErrorActivityIsDisplayed() { + onView(withId(R.id.error_message)).check(matches(isDisplayed())) + onView(withId(R.id.retry_button)).check(matches(isDisplayed())) + } + + @Test + fun testRetryButton() { + onView(withId(R.id.retry_button)).perform(click()) + + val intendedIntent = Intent(getInstrumentation().targetContext, Map_Activity::class.java) + intendedIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + assertEquals(intendedIntent.component, (getInstrumentation().context as AppCompatActivity)) + } +} diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt new file mode 100644 index 00000000..e284ac73 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt @@ -0,0 +1,84 @@ +package campus.tech.kakao.map + +import android.content.SharedPreferences +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.View.Map_Activity +import com.google.android.gms.maps.model.LatLng +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MapActivityTest { + + private lateinit var scenario: ActivityScenario + private lateinit var sharedPreferences: SharedPreferences + private lateinit var editor: SharedPreferences.Editor + + @Before + fun setup() { + editor = sharedPreferences.edit() + + scenario = ActivityScenario.launch(Map_Activity::class.java) + } + + @After + fun tearDown() { + scenario.close() + } + + @Test + fun mapSuccess() { + scenario.onActivity { activity -> + val googleMap = scenario + assertNotNull(googleMap) + } + } + + @Test + fun lastLocationRequest() { + val testLatLng = LatLng(37.5665, 126.9780) + + scenario.onActivity { activity -> + val method = Map_Activity::class.java.getDeclaredMethod("saveLastLocation", Double::class.java, Double::class.java) + method.isAccessible = true + method.invoke(activity, testLatLng.latitude, testLatLng.longitude) + + val lastLatitude = sharedPreferences.getFloat("lastLatitude", 0f) + val lastLongitude = sharedPreferences.getFloat("lastLongitude", 0f) + + assertEquals(testLatLng.latitude.toFloat(), lastLatitude) + assertEquals(testLatLng.longitude.toFloat(), lastLongitude) + + val field = Map_Activity::class.java.getDeclaredField("lastKnownLocation") + field.isAccessible = true + val lastKnownLocation = field.get(activity) as LatLng + + assertEquals(testLatLng, lastKnownLocation) + } + } + + @Test + fun showError401() { + scenario.onActivity { activity -> + activity.onMapError(401) + onView(withId(R.id.error_message)).check(matches(isDisplayed())) + onView(withId(R.id.error_message)).check(matches(withText("401 Unauthorized Error"))) + } + } +} + + + + + + diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt new file mode 100644 index 00000000..03536ffe --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt @@ -0,0 +1,53 @@ +@file:Suppress("DEPRECATION") + +package campus.tech.kakao.map + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.runner.AndroidJUnit4 +import campus.tech.kakao.map.View.Search_Activity +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SearchActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(Search_Activity::class.java) + + @Test + fun testSearchView_isDisplayed() { + onView(withId(R.id.search_text)).check(matches(isDisplayed())) + } + + @Test + fun testSearchRecyclerView_isDisplayed() { + onView(withId(R.id.RecyclerVer)).check(matches(isDisplayed())) + } + + @Test + fun testSavedSearchRecyclerView_isDisplayed() { + onView(withId(R.id.recyclerHor)).check(matches(isDisplayed())) + } + + @Test + fun testSearchView_textEntry() { + onView(withId(R.id.search_text)) + .perform(typeText("카페"), pressImeActionButton()) + + onView(withId(R.id.RecyclerVer)).check(matches(hasDescendant(withText("Cafe Name")))) + } + + @Test + fun testSearchResult_click() { + onView(withId(R.id.search_text)) + .perform(typeText("도서관"), pressImeActionButton()) + + Thread.sleep(2000) + } + +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..393e65d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,12 @@ + + + + + + + + + - + + diff --git a/app/src/main/java/campus/tech/kakao/map/Adapter/SavedSearchAdapter.kt b/app/src/main/java/campus/tech/kakao/map/Adapter/SavedSearchAdapter.kt new file mode 100644 index 00000000..8a0243e4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Adapter/SavedSearchAdapter.kt @@ -0,0 +1,56 @@ +package campus.tech.kakao.map.Adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R + +open class SavedSearchAdapter( + private val context: Context, + private var data: List, + private val onItemClick: (String) -> Unit +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchViewHolder { + val view = LayoutInflater.from(context).inflate(R.layout.activity_item_view, parent, false) + return SavedSearchViewHolder(view, onItemClick) + } + + override fun onBindViewHolder(holder: SavedSearchViewHolder, position: Int) { + holder.bind(data[position]) + } + + override fun getItemCount(): Int = data.size + + fun updateData(newData: List) { + data = newData + notifyDataSetChanged() + } + + open inner class SavedSearchViewHolder(itemView: View, private val onItemClick: (String) -> Unit) : + RecyclerView.ViewHolder(itemView) { + + private val textView: TextView = itemView.findViewById(R.id.result) + private val deleteButton: Button = itemView.findViewById(R.id.delete) + + open fun bind(searchText: String) { + textView.text = searchText + itemView.setOnClickListener { + onItemClick(searchText) + } + deleteButton.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + val updatedData = data.toMutableList() + updatedData.removeAt(position) + data = updatedData.toList() + notifyDataSetChanged() + } + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/Data/AppDatabase.kt b/app/src/main/java/campus/tech/kakao/map/Data/AppDatabase.kt new file mode 100644 index 00000000..7518b75a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/AppDatabase.kt @@ -0,0 +1,27 @@ +package campus.tech.kakao.map.Data +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [SearchResult::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun searchDao(): SearchDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "History.db" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/Data/Document.kt b/app/src/main/java/campus/tech/kakao/map/Data/Document.kt new file mode 100644 index 00000000..c33097e3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/Document.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.Data + +import com.google.gson.annotations.SerializedName + +data class Document( + @SerializedName("category_group_name") val categoryName: String, + @SerializedName("id") val id: String, + @SerializedName("address_name") val addressName: String, + @SerializedName("place_name") val placeName: String? +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/Data/Place.kt b/app/src/main/java/campus/tech/kakao/map/Data/Place.kt new file mode 100644 index 00000000..ed8473ae --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/Place.kt @@ -0,0 +1,43 @@ +package campus.tech.kakao.map.Data + +import android.os.Parcel +import android.os.Parcelable + +data class Place ( + val place_name: String, + val address_name: String, + val category_group_name: String, + val latitude: Double, + val longitude: Double +) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString()!!, + parcel.readString()!!, + parcel.readString()!!, + parcel.readDouble(), + parcel.readDouble() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(place_name) + parcel.writeString(address_name) + parcel.writeString(category_group_name) + parcel.writeDouble(latitude) + parcel.writeDouble(longitude) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Place { + return Place(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/Data/ResultSearchKeyword.kt b/app/src/main/java/campus/tech/kakao/map/Data/ResultSearchKeyword.kt new file mode 100644 index 00000000..47ed213e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/ResultSearchKeyword.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.Data + +import campus.tech.kakao.map.Data.Place + +data class ResultSearchKeyword( + var document: List +) diff --git a/app/src/main/java/campus/tech/kakao/map/Data/SearchDao.kt b/app/src/main/java/campus/tech/kakao/map/Data/SearchDao.kt new file mode 100644 index 00000000..846e65fb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/SearchDao.kt @@ -0,0 +1,15 @@ +package campus.tech.kakao.map.Data + +import androidx.room.* + +@Dao +interface SearchDao { + @Insert + suspend fun insertSearchResult(searchResult: SearchResult) + + @Delete + suspend fun deleteSearchResult(searchResult: SearchResult) + + @Query("SELECT * FROM search_results WHERE text LIKE :searchText") + suspend fun getSearchResults(searchText: String): List +} diff --git a/app/src/main/java/campus/tech/kakao/map/Data/SearchRepository.kt b/app/src/main/java/campus/tech/kakao/map/Data/SearchRepository.kt new file mode 100644 index 00000000..0fe69f6a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/SearchRepository.kt @@ -0,0 +1,15 @@ +package campus.tech.kakao.map.Data + +class SearchRepository(private val searchDao: SearchDao) { + suspend fun insertSearchResult(text: String) { + searchDao.insertSearchResult(SearchResult(text = text)) + } + + suspend fun deleteSearchResult(searchResult: SearchResult) { + searchDao.deleteSearchResult(searchResult) + } + + suspend fun getSearchResults(text: String): List { + return searchDao.getSearchResults("%$text%") + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/Data/SearchResult.kt b/app/src/main/java/campus/tech/kakao/map/Data/SearchResult.kt new file mode 100644 index 00000000..51f28aa4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/SearchResult.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.Data + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "search_results") +data class SearchResult( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val text: String +) diff --git a/app/src/main/java/campus/tech/kakao/map/Data/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/Data/SearchViewModel.kt new file mode 100644 index 00000000..13963a98 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Data/SearchViewModel.kt @@ -0,0 +1,37 @@ +package campus.tech.kakao.map.Data + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class SearchViewModel(application: Application) : AndroidViewModel(application) { + private val repository: SearchRepository + + init { + val searchDao = AppDatabase.getDatabase(application).searchDao() + repository = SearchRepository(searchDao) + } + + fun insertSearchResult(text: String) = viewModelScope.launch { + repository.insertSearchResult(text) + } + + fun deleteSearchResult(searchResult: SearchResult) = viewModelScope.launch { + repository.deleteSearchResult(searchResult) + } + + private val searchresults = MutableLiveData>() + val searchResults: LiveData> = searchresults + + fun getSearchResults(text: String) = viewModelScope.launch { + searchresults.postValue(repository.getSearchResults(text)) + } +} + + + + + diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/View/AppModule.kt b/app/src/main/java/campus/tech/kakao/map/View/AppModule.kt new file mode 100644 index 00000000..787ad1db --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/AppModule.kt @@ -0,0 +1,23 @@ +package campus.tech.kakao.map.View + +import android.content.Context +import androidx.test.espresso.core.internal.deps.dagger.Module +import androidx.test.espresso.core.internal.deps.dagger.Provides +import com.google.android.libraries.places.api.Places +import com.google.android.libraries.places.api.net.PlacesClient +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun providePlacesClient(@ApplicationContext context: Context): PlacesClient { + Places.initialize(context, "AIzaSyCUncz7v8nwT3m5OHasVJTep1e1549yAKM") + return Places.createClient(context) + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/View/ErrorActivity.kt b/app/src/main/java/campus/tech/kakao/map/View/ErrorActivity.kt new file mode 100644 index 00000000..3ecf2423 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/ErrorActivity.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.View + +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ErrorActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_error) + + val retryButton: Button = findViewById(R.id.retry_button) + retryButton.setOnClickListener { + val intent = Intent(this, Map_Activity::class.java) + startActivity(intent) + finish() + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/View/Map_Activity.kt b/app/src/main/java/campus/tech/kakao/map/View/Map_Activity.kt new file mode 100644 index 00000000..b261190a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/Map_Activity.kt @@ -0,0 +1,131 @@ +package campus.tech.kakao.map.View + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import campus.tech.kakao.map.Data.Place +import campus.tech.kakao.map.R +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.MapView +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class Map_Activity : AppCompatActivity(), OnMapReadyCallback { + + private lateinit var mapView: MapView + private lateinit var googleMap: GoogleMap + private lateinit var searchView: SearchView + private lateinit var lastKnownLocation: LatLng + + @Inject + lateinit var preferences: SharedPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_map) + + mapView = findViewById(R.id.map_view) + searchView = findViewById(R.id.search_text) + + mapView.onCreate(savedInstanceState) + mapView.getMapAsync(this) + + val lat = preferences.getFloat("lastLatitude", 0f).toDouble() + val lng = preferences.getFloat("lastLongitude", 0f).toDouble() + lastKnownLocation = LatLng(lat, lng) + + setupSearchView() + } + + private fun setupSearchView() { + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (hasFocus) { + val intent = Intent(this, Search_Activity::class.java) + startActivity(intent) + searchView.clearFocus() + } + } + } + + override fun onMapReady(map: GoogleMap) { + googleMap = map + + val selectedPlace = intent.getParcelableExtra("selectedPlace") + + val initialLatLng = if (selectedPlace != null) { + LatLng(selectedPlace.latitude, selectedPlace.longitude).also { + addMarkerAndMoveCamera(it, selectedPlace.place_name, selectedPlace.address_name) + } + } else { + lastKnownLocation + } + + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(initialLatLng, 15f)) + + googleMap.setOnMapClickListener { latLng -> + googleMap.clear() + googleMap.addMarker(MarkerOptions().position(latLng).title("Selected Location")) + saveLastLocation(latLng.latitude, latLng.longitude) + } + } + + private fun addMarkerAndMoveCamera(latLng: LatLng, title: String?, snippet: String?) { + googleMap.clear() + googleMap.addMarker(MarkerOptions().position(latLng).title(title).snippet(snippet)) + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f)) + } + + private fun saveLastLocation(latitude: Double, longitude: Double) { + with(preferences.edit()) { + putFloat("lastLatitude", latitude.toFloat()) + putFloat("lastLongitude", longitude.toFloat()) + apply() + } + } + + override fun onResume() { + super.onResume() + mapView.onResume() + } + + override fun onPause() { + super.onPause() + mapView.onPause() + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } + + fun onMapError(errorCode: Int) { + if (errorCode == 401) { + val intent = Intent(this, ErrorActivity::class.java).apply { + putExtra("error_message", "401 Unauthorized Error") + } + startActivity(intent) + } + } + +} + + + diff --git a/app/src/main/java/campus/tech/kakao/map/View/Search_Activity.kt b/app/src/main/java/campus/tech/kakao/map/View/Search_Activity.kt new file mode 100644 index 00000000..b86c8d63 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/Search_Activity.kt @@ -0,0 +1,197 @@ +package campus.tech.kakao.map.View + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.libraries.places.api.Places +import com.google.android.libraries.places.api.model.Place +import com.google.android.libraries.places.api.net.PlacesClient +import androidx.appcompat.widget.SearchView +import campus.tech.kakao.map.Adapter.SavedSearchAdapter +import campus.tech.kakao.map.R +import com.google.android.libraries.places.api.net.FetchPlaceRequest +import androidx.activity.viewModels +import androidx.lifecycle.Observer +import campus.tech.kakao.map.Data.SearchViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class Search_Activity : AppCompatActivity() { + + @Inject + lateinit var placesClient: PlacesClient + + private lateinit var searchView: SearchView + private lateinit var searchRecyclerView: RecyclerView + private lateinit var searchResultAdapter: PlaceAdapter + private lateinit var savedSearchRecyclerView: RecyclerView + private lateinit var savedSearchAdapter: SavedSearchAdapter + private val searchViewModel: SearchViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + + Places.initialize(applicationContext, "AIzaSyCUncz7v8nwT3m5OHasVJTep1e1549yAKM") + + initViews() + initAdapters() + setupRecyclerViews() + setupSearchView() + + observeSearchResults() + observeSavedSearches() + } + + private fun initViews() { + searchView = findViewById(R.id.search_text) + searchRecyclerView = findViewById(R.id.RecyclerVer) + savedSearchRecyclerView = findViewById(R.id.recyclerHor) + } + + private fun initAdapters() { + searchResultAdapter = PlaceAdapter(emptyList()) + savedSearchAdapter = SavedSearchAdapter(this, emptyList()) { searchText -> + searchAndDisplayResults(searchText) + } + } + + private fun setupRecyclerViews() { + searchRecyclerView.layoutManager = LinearLayoutManager(this) + savedSearchRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + + searchRecyclerView.adapter = searchResultAdapter + savedSearchRecyclerView.adapter = savedSearchAdapter + } + + private fun setupSearchView() { + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + if (!query.isNullOrBlank()) { + searchViewModel.insertSearchResult(text = query) + searchAndDisplayResults(query) + } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + if (newText.isNullOrBlank()) { + searchAndDisplayResults("") + } else { + searchAndDisplayResults(newText) + } + return true + } + }) + } + + private fun searchAndDisplayResults(searchText: String) { + if (searchText.isBlank()) { + searchRecyclerView.visibility = RecyclerView.GONE + return + } + } + + private fun observeSearchResults() { + searchViewModel.searchResults.observe(this, Observer { results -> + if (results.isEmpty()) { + searchRecyclerView.visibility = RecyclerView.GONE + } else { + val places = results.map { result -> + mapOf( + "name" to result.text, + "address" to "", + "id" to result.id.toString() + ) + } + searchResultAdapter.updateData(places) + searchRecyclerView.visibility = RecyclerView.VISIBLE + } + }) + } + + private fun observeSavedSearches() { + searchViewModel.searchResults.observe(this, Observer { savedSearches -> + savedSearchAdapter.notifyDataSetChanged() + }) + } + + inner class PlaceAdapter(private var data: List>) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_place_item, parent, false) + return PlaceViewHolder(view) + } + + override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) { + val place = data[position] + holder.bind(place) + } + + override fun getItemCount(): Int = data.size + + fun updateData(newData: List>) { + data = newData + notifyDataSetChanged() + } + + inner class PlaceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val nameTextView: TextView = itemView.findViewById(R.id.name) + private val addressTextView: TextView = itemView.findViewById(R.id.place) + + fun bind(place: Map) { + nameTextView.text = place["name"] ?: "No Name" + addressTextView.text = place["address"] ?: "No Address" + + itemView.setOnClickListener { + val placeId = place["id"] + if (!placeId.isNullOrBlank()) { + fetchPlaceDetails(placeId) + } + } + } + + private fun fetchPlaceDetails(placeId: String) { + val placeFields = listOf(Place.Field.ID, Place.Field.NAME, Place.Field.ADDRESS, Place.Field.LAT_LNG) + + val request = FetchPlaceRequest.builder(placeId, placeFields).build() + placesClient.fetchPlace(request).addOnSuccessListener { response -> + val place = response.place + val latLng = place.latLng + + if (latLng != null) { + val placeName = place.name ?: "Unknown" + val placeAddress = place.address ?: "Unknown" + + val intent = Intent(itemView.context, Map_Activity::class.java).apply { + putExtra("selectedPlace", campus.tech.kakao.map.Data.Place(placeName, placeAddress, "", latLng.latitude, latLng.longitude)) + } + itemView.context.startActivity(intent) + } else { + Log.e("FetchPlaceDetails", "latLng is null for placeId: $placeId") + } + }.addOnFailureListener { exception -> + exception.printStackTrace() + Toast.makeText(itemView.context, "Failed to fetch place details", Toast.LENGTH_LONG).show() + } + } + } + } +} + + + + + + + + diff --git a/app/src/main/java/campus/tech/kakao/map/View/mapModule.kt b/app/src/main/java/campus/tech/kakao/map/View/mapModule.kt new file mode 100644 index 00000000..7ff280da --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/mapModule.kt @@ -0,0 +1,21 @@ +package campus.tech.kakao.map.View + +import android.content.Context +import android.content.SharedPreferences +import androidx.test.espresso.core.internal.deps.dagger.Module +import androidx.test.espresso.core.internal.deps.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 mapModule { + + @Provides + @Singleton + fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("MapPrefs", Context.MODE_PRIVATE) + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/View/myApplication.kt b/app/src/main/java/campus/tech/kakao/map/View/myApplication.kt new file mode 100644 index 00000000..144af346 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/View/myApplication.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.View + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class myApplication : Application() \ No newline at end of file diff --git a/app/src/main/res/drawable/close.png b/app/src/main/res/drawable/close.png new file mode 100644 index 00000000..45e82a40 Binary files /dev/null and b/app/src/main/res/drawable/close.png differ diff --git a/app/src/main/res/drawable/placeholder.png b/app/src/main/res/drawable/placeholder.png new file mode 100644 index 00000000..2be902f2 Binary files /dev/null and b/app/src/main/res/drawable/placeholder.png differ diff --git a/app/src/main/res/layout/activity_error.xml b/app/src/main/res/layout/activity_error.xml new file mode 100644 index 00000000..e2883dd9 --- /dev/null +++ b/app/src/main/res/layout/activity_error.xml @@ -0,0 +1,35 @@ + + + + + + + + +