Skip to content

Commit 314e7b9

Browse files
committed
Merge branch 'feature/47-dropdown-shows-very-limited-number-of' into develop
2 parents c0f27a8 + 2197ae8 commit 314e7b9

File tree

10 files changed

+184
-51
lines changed

10 files changed

+184
-51
lines changed

app/build.gradle

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ plugins {
88

99
android {
1010
namespace 'com.mapcode'
11-
compileSdk 32
11+
compileSdk 33
1212

1313
defaultConfig {
1414
applicationId "com.mapcode"
1515
minSdk 21
1616
//noinspection OldTargetApi wait for sources to be released before upgrading
17-
targetSdk 32
17+
targetSdk 33
1818
versionCode 3
1919
versionName "1.1.0"
2020

@@ -64,6 +64,8 @@ dependencies {
6464
//google
6565
implementation 'com.google.maps.android:maps-compose:2.2.0'
6666
implementation 'com.google.android.gms:play-services-maps:18.1.0'
67+
implementation 'com.google.android.libraries.places:places:2.6.0'
68+
implementation 'com.google.maps.android:places-ktx:2.0.0'
6769
implementation "com.google.dagger:hilt-android:2.42"
6870
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
6971
implementation 'com.google.android.gms:play-services-location:20.0.0'
@@ -77,15 +79,15 @@ dependencies {
7779
implementation 'androidx.activity:activity-compose:1.5.1'
7880
//noinspection GradleDependency
7981
implementation "androidx.compose.ui:ui:1.2.0"
80-
implementation "androidx.compose.ui:ui-tooling-preview:1.2.0"
81-
implementation 'androidx.compose.material:material:1.2.0'
82+
implementation "androidx.compose.ui:ui-tooling-preview:1.2.1"
83+
implementation 'androidx.compose.material:material:1.2.1'
8284
implementation "androidx.navigation:navigation-compose:2.5.1"
83-
implementation "androidx.compose.foundation:foundation:1.3.0-alpha02"
85+
implementation "androidx.compose.foundation:foundation:1.3.0-alpha03"
8486
implementation 'com.google.accompanist:accompanist-permissions:0.24.10-beta'
8587
implementation "com.google.accompanist:accompanist-systemuicontroller:0.17.0"
8688
implementation "androidx.datastore:datastore-preferences:1.0.0"
8789
implementation "androidx.core:core-splashscreen:1.0.0"
88-
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha15"
90+
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha16"
8991

9092
//other
9193
implementation 'com.jakewharton.timber:timber:5.0.1'
@@ -94,8 +96,8 @@ dependencies {
9496
implementation "com.squareup.okhttp3:okhttp:4.10.0"
9597

9698
//debug
97-
debugImplementation "androidx.compose.ui:ui-tooling:1.2.0"
98-
debugImplementation "androidx.compose.ui:ui-test-manifest:1.2.0"
99+
debugImplementation "androidx.compose.ui:ui-tooling:1.2.1"
100+
debugImplementation "androidx.compose.ui:ui-test-manifest:1.2.1"
99101

100102
//testing
101103
testImplementation 'junit:junit:4.13.2'
@@ -110,7 +112,7 @@ dependencies {
110112
androidTestImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.25'
111113
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
112114
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
113-
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.2.0"
115+
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.2.1"
114116
androidTestImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
115117
androidTestImplementation 'org.mockito:mockito-android:4.6.1'
116118
androidTestImplementation 'androidx.test:rules:1.4.0'

app/src/androidTest/java/com/mapcode/map/FakeShowMapcodeUseCase.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,16 @@ class FakeShowMapcodeUseCase : ShowMapcodeUseCase {
9797
sharedText = text
9898
}
9999

100-
override fun launchDirectionsToLocation(location: Location, zoom: Float): Boolean {
101-
return isMapsAppInstalled
100+
override suspend fun getMatchingAddresses(
101+
query: String,
102+
maxResults: Int,
103+
southwest: Location,
104+
northeast: Location
105+
): Result<List<String>> {
106+
return success(matchingAddresses[query] ?: emptyList())
102107
}
103108

104-
override suspend fun getMatchingAddresses(query: String): Result<List<String>> {
105-
return success(matchingAddresses[query] ?: emptyList())
109+
override fun launchDirectionsToLocation(location: Location, zoom: Float): Boolean {
110+
return isMapsAppInstalled
106111
}
107112
}

app/src/androidTest/java/com/mapcode/map/MapScreenTest.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ class MapScreenTest {
183183
composeTestRule.waitForIdle()
184184

185185
composeTestRule.onNodeWithText("Street, City, Country").assertIsDisplayed()
186-
187186
composeTestRule.onNodeWithText("City, Country").assertIsDisplayed()
188187
}
189188

@@ -530,6 +529,10 @@ class MapScreenTest {
530529
performTextInput("address")
531530
}
532531

532+
composeTestRule.waitUntil(2000) {
533+
viewModel.uiState.value.addressUi.matchingAddresses.isNotEmpty()
534+
}
535+
533536
composeTestRule.waitForIdle()
534537
composeTestRule.onNodeWithTag("address_dropdown").assertIsDisplayed()
535538
composeTestRule.onNodeWithText("Street 1").assertIsDisplayed()
@@ -572,7 +575,10 @@ class MapScreenTest {
572575
performTextInput("address")
573576
}
574577

575-
composeTestRule.waitForIdle()
578+
composeTestRule.waitUntil(2000) {
579+
viewModel.uiState.value.addressUi.matchingAddresses.isNotEmpty()
580+
}
581+
576582
composeTestRule.onNodeWithText("Street 1").performClick()
577583
composeTestRule.onNodeWithText("Enter address or mapcode").assertIsNotFocused()
578584
}

app/src/main/java/com/mapcode/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
2828
import androidx.core.view.WindowCompat
2929
import androidx.navigation.compose.rememberNavController
3030
import com.google.android.gms.maps.MapsInitializer
31+
import com.google.android.libraries.places.api.Places
3132
import com.mapcode.map.MapViewModel
3233
import com.mapcode.theme.MapcodeTheme
3334
import dagger.hilt.android.AndroidEntryPoint
@@ -48,6 +49,7 @@ class MainActivity : ComponentActivity() {
4849
MapcodeApp(viewModel, windowSizeClass)
4950
}
5051

52+
Places.initialize(applicationContext, BuildConfig.MAPS_API_KEY)
5153
MapsInitializer.initialize(this)
5254
viewModel.isGoogleMapsSdkLoaded = true
5355
}

app/src/main/java/com/mapcode/map/MapViewModel.kt

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,8 @@ import com.mapcode.data.Keys
3434
import com.mapcode.data.PreferenceRepository
3535
import com.mapcode.util.*
3636
import dagger.hilt.android.lifecycle.HiltViewModel
37-
import kotlinx.coroutines.Job
38-
import kotlinx.coroutines.delay
37+
import kotlinx.coroutines.*
3938
import kotlinx.coroutines.flow.*
40-
import kotlinx.coroutines.launch
41-
import kotlinx.coroutines.runBlocking
4239
import java.io.IOException
4340
import java.math.RoundingMode
4441
import java.text.DecimalFormat
@@ -56,7 +53,8 @@ class MapViewModel @Inject constructor(
5653

5754
companion object {
5855
private const val UNKNOWN_ADDRESS_ERROR_TIMEOUT: Long = 3000
59-
const val ANIMATE_CAMERA_UPDATE_DURATION_MS = 200
56+
const val ANIMATE_CAMERA_UPDATE_DURATION_MS: Int = 200
57+
private const val AUTOCOMPLETE_ADDRESS_DELAY_MS: Long = 1500
6058
}
6159

6260
private val latLngNumberFormat: NumberFormat by lazy { NumberFormat.getNumberInstance(Locale.getDefault()) }
@@ -217,12 +215,49 @@ class MapViewModel @Inject constructor(
217215
}
218216

219217
fun onAddressTextChange(text: String) {
220-
addressUi.update { it.copy(address = text) }
218+
//if the user is continuing the same search query then do not clear the matching addresses
219+
val clearMatchingAddresses = text.dropLast(1) != addressUi.value.address
220+
221+
addressUi.update {
222+
if (clearMatchingAddresses) {
223+
it.copy(address = text, matchingAddresses = emptyList())
224+
} else {
225+
it.copy(address = text)
226+
}
227+
}
221228

222229
getMatchingAddressesJob?.cancel()
223-
getMatchingAddressesJob = viewModelScope.launch {
224-
useCase.getMatchingAddresses(text).onSuccess { result ->
225-
addressUi.update { it.copy(matchingAddresses = result) }
230+
231+
if (text.isNotEmpty()) {
232+
getMatchingAddressesJob = viewModelScope.launch(dispatchers.default) {
233+
delay(AUTOCOMPLETE_ADDRESS_DELAY_MS)
234+
235+
val latLngBounds = withContext(dispatchers.main) {
236+
cameraPositionState.projection?.visibleRegion?.latLngBounds
237+
}
238+
val maxResults = 10
239+
240+
val matchingAddresses = if (latLngBounds != null) {
241+
useCase.getMatchingAddresses(
242+
text,
243+
maxResults = maxResults,
244+
southwest = Location(latLngBounds.southwest.latitude, latLngBounds.southwest.longitude),
245+
northeast = Location(latLngBounds.northeast.latitude, latLngBounds.northeast.longitude)
246+
).getOrNull()
247+
} else {
248+
useCase.getMatchingAddresses(
249+
text,
250+
maxResults = maxResults,
251+
southwest = Location.GLOBE_SOUTH_WEST,
252+
northeast = Location.GLOBE_NORTH_EAST
253+
).getOrNull()
254+
}
255+
256+
if (matchingAddresses == null) {
257+
return@launch
258+
}
259+
260+
addressUi.update { it.copy(matchingAddresses = matchingAddresses.distinct()) }
226261
}
227262
}
228263
}

app/src/main/java/com/mapcode/map/ShowMapcodeUseCase.kt

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ import android.content.*
2121
import android.location.Geocoder
2222
import android.net.Uri
2323
import androidx.core.content.getSystemService
24+
import com.google.android.gms.common.api.ApiException
2425
import com.google.android.gms.location.FusedLocationProviderClient
2526
import com.google.android.gms.location.LocationServices
27+
import com.google.android.gms.maps.model.LatLng
28+
import com.google.android.libraries.places.api.Places
29+
import com.google.android.libraries.places.api.model.RectangularBounds
30+
import com.google.android.libraries.places.api.net.PlacesClient
31+
import com.google.android.libraries.places.ktx.api.net.awaitFindAutocompletePredictions
2632
import com.mapcode.Mapcode
2733
import com.mapcode.MapcodeCodec
2834
import com.mapcode.UnknownMapcodeException
@@ -31,10 +37,7 @@ import com.mapcode.util.Location
3137
import com.mapcode.util.NoAddressException
3238
import com.mapcode.util.UnknownAddressException
3339
import dagger.hilt.android.qualifiers.ApplicationContext
34-
import kotlinx.coroutines.CoroutineScope
35-
import kotlinx.coroutines.Dispatchers
36-
import kotlinx.coroutines.launch
37-
import kotlinx.coroutines.withContext
40+
import kotlinx.coroutines.*
3841
import okhttp3.HttpUrl
3942
import okhttp3.OkHttpClient
4043
import okhttp3.Request
@@ -62,6 +65,8 @@ class ShowMapcodeUseCaseImpl @Inject constructor(
6265
*/
6366
private var cachedLastLocation: Location? = null
6467

68+
private val placesClient: PlacesClient by lazy { Places.createClient(ctx) }
69+
6570
override fun getMapcodes(lat: Double, long: Double): List<Mapcode> {
6671
coroutineScope.launch(Dispatchers.IO) {
6772
try {
@@ -103,7 +108,7 @@ class ShowMapcodeUseCaseImpl @Inject constructor(
103108
}
104109

105110
val matchingAddress = withContext(Dispatchers.Default) {
106-
geocoder.getFromLocationName(address, 10).firstOrNull()
111+
geocoder.getFromLocationName(address, 10)?.firstOrNull()
107112
}
108113

109114
if (matchingAddress == null) {
@@ -119,7 +124,7 @@ class ShowMapcodeUseCaseImpl @Inject constructor(
119124
override suspend fun reverseGeocode(lat: Double, long: Double): Result<String> {
120125
try {
121126
val addressList = withContext(Dispatchers.Default) {
122-
geocoder.getFromLocation(lat, long, 1)
127+
geocoder.getFromLocation(lat, long, 1) ?: emptyList()
123128
}
124129

125130
if (addressList.isEmpty()) {
@@ -193,23 +198,36 @@ class ShowMapcodeUseCaseImpl @Inject constructor(
193198
ctx.startActivity(shareIntent)
194199
}
195200

196-
override suspend fun getMatchingAddresses(query: String): Result<List<String>> {
197-
try {
198-
val addressList = withContext(Dispatchers.Default) {
199-
geocoder.getFromLocationName(query, 3)
200-
}
201-
202-
val addressStringList = addressList.map { address ->
203-
buildString {
204-
for (i in 0..address.maxAddressLineIndex) {
205-
append(address.getAddressLine(i))
206-
}
201+
@OptIn(ExperimentalCoroutinesApi::class)
202+
override suspend fun getMatchingAddresses(
203+
query: String,
204+
maxResults: Int,
205+
southwest: Location,
206+
northeast: Location
207+
): Result<List<String>> {
208+
val locationBias = RectangularBounds.newInstance(
209+
LatLng(southwest.latitude, southwest.longitude),
210+
LatLng(northeast.latitude, northeast.longitude)
211+
)
212+
213+
val responseResult = withContext(Dispatchers.Default) {
214+
try {
215+
val response = placesClient.awaitFindAutocompletePredictions {
216+
this.query = query
217+
this.locationBias = locationBias
207218
}
219+
220+
success(response)
221+
} catch (e: ApiException) {
222+
Timber.e(e.status.toString())
223+
failure(e)
208224
}
225+
}
209226

210-
return success(addressStringList)
211-
} catch (e: IOException) {
212-
return failure(e)
227+
return responseResult.map { response ->
228+
response.autocompletePredictions
229+
.map { it.getFullText(null) }
230+
.map { it.toString() }
213231
}
214232
}
215233
}
@@ -267,7 +285,12 @@ interface ShowMapcodeUseCase {
267285
fun shareText(text: String, description: String)
268286

269287
/**
270-
* Get a list of addresses that might correspond to the [query].
288+
* Get a list of addresses within the [northeast] and [southwest] bounds that might correspond to the [query].
271289
*/
272-
suspend fun getMatchingAddresses(query: String): Result<List<String>>
290+
suspend fun getMatchingAddresses(
291+
query: String,
292+
maxResults: Int,
293+
southwest: Location,
294+
northeast: Location
295+
): Result<List<String>>
273296
}

app/src/main/java/com/mapcode/util/Location.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@
1616

1717
package com.mapcode.util
1818

19-
data class Location(val latitude: Double, val longitude: Double)
19+
data class Location(val latitude: Double, val longitude: Double) {
20+
companion object {
21+
val GLOBE_SOUTH_WEST: Location = Location(-90.0, -180.0)
22+
val GLOBE_NORTH_EAST: Location = Location(90.0, 180.0)
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (C) 2022, Stichting Mapcode Foundation (http://www.mapcode.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mapcode
18+
19+
import com.mapcode.util.Location
20+
21+
data class LocalAddressQuery(val query: String, val southwest: Location, val northeast: Location)

app/src/test/java/com/mapcode/map/FakeShowMapcodeUseCase.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,13 @@ class FakeShowMapcodeUseCase : ShowMapcodeUseCase {
103103
override fun shareText(text: String, description: String) {
104104
sharedText = text
105105
}
106-
107-
override suspend fun getMatchingAddresses(query: String): Result<List<String>> {
108-
return success(matchingAddresses[query] ?: emptyList())
106+
107+
override suspend fun getMatchingAddresses(
108+
query: String,
109+
maxResults: Int,
110+
southwest: Location,
111+
northeast: Location
112+
): Result<List<String>> {
113+
return success((matchingAddresses[query] ?: emptyList()))
109114
}
110115
}

0 commit comments

Comments
 (0)