diff --git a/CHANGELOG.md b/CHANGELOG.md index 5379258b8..f5d191e00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog for the Mapbox Search SDK for Android +## 2.6.1 + +### New features +- Added experimental Details API that provides access to POI metadata, boundary details, addresses and places. See `DetailsApi` type. + +### Bug fixes +- Fixed `OpenHours` parsing for the Search Box Api type. + +### Mapbox dependencies +- Search Native SDK `2.6.2` +- Common SDK `24.8.0` + + + ## 2.6.0 ### Mapbox dependencies diff --git a/MapboxSearch/base/src/main/java/com/mapbox/search/base/ExperimentalMapboxSearchAPI.kt b/MapboxSearch/base/src/main/java/com/mapbox/search/base/ExperimentalMapboxSearchAPI.kt new file mode 100644 index 000000000..c5b7dfec9 --- /dev/null +++ b/MapboxSearch/base/src/main/java/com/mapbox/search/base/ExperimentalMapboxSearchAPI.kt @@ -0,0 +1,19 @@ +package com.mapbox.search.base + +/** + * This annotation marks the experimental state of the Mapbox Search SDK API. + * This API is stable in nature, but it's likely that properties might be added or removed in the + * future. + * Any usage of a declaration annotated with `@ExperimentalMapboxSearchAPI` must be accepted + * either by annotating that usage with the [OptIn] annotation, + * e.g. `@OptIn(ExperimentalMapboxSearchAPI::class)`, or by using the compiler argument + * `-Xopt-in=com.mapbox.search.base.ExperimentalMapboxSearchAPI`. + */ +@Retention(value = AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, +) +annotation class ExperimentalMapboxSearchAPI diff --git a/MapboxSearch/gradle.properties b/MapboxSearch/gradle.properties index 52f0ccf55..c31ec12f1 100644 --- a/MapboxSearch/gradle.properties +++ b/MapboxSearch/gradle.properties @@ -21,7 +21,7 @@ android.enableJetifier=false kotlin.code.style=official # SDK version attributes -VERSION_NAME=2.6.0 +VERSION_NAME=2.6.1 # Artifact attributes mapboxArtifactUserOrg=mapbox diff --git a/MapboxSearch/gradle/versions.gradle b/MapboxSearch/gradle/versions.gradle index d0d39fa3c..038d65e36 100644 --- a/MapboxSearch/gradle/versions.gradle +++ b/MapboxSearch/gradle/versions.gradle @@ -47,7 +47,7 @@ ext { common_sdk_version = '24.8.0' mapbox_base_version = '0.8.0' - search_native_version = '2.6.0' + search_native_version = '2.6.2' detekt_version = '1.19.0' diff --git a/MapboxSearch/sample/src/main/AndroidManifest.xml b/MapboxSearch/sample/src/main/AndroidManifest.xml index e42852470..bccc82d09 100644 --- a/MapboxSearch/sample/src/main/AndroidManifest.xml +++ b/MapboxSearch/sample/src/main/AndroidManifest.xml @@ -68,6 +68,7 @@ + diff --git a/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/MainActivity.kt b/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/MainActivity.kt index b4502cfa2..3999af88e 100644 --- a/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/MainActivity.kt +++ b/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/MainActivity.kt @@ -56,6 +56,7 @@ import com.mapbox.search.sample.api.CategorySearchJavaExampleActivity import com.mapbox.search.sample.api.CategorySearchKotlinExampleActivity import com.mapbox.search.sample.api.CustomIndexableDataProviderJavaExample import com.mapbox.search.sample.api.CustomIndexableDataProviderKotlinExample +import com.mapbox.search.sample.api.DetailsApiKotlinExampleActivity import com.mapbox.search.sample.api.DiscoverJavaExampleActivity import com.mapbox.search.sample.api.DiscoverKotlinExampleActivity import com.mapbox.search.sample.api.FavoritesDataProviderJavaExample @@ -477,6 +478,10 @@ class MainActivity : AppCompatActivity() { startActivity(Intent(this, JapanSearchKotlinExampleActivity::class.java)) true } + R.id.open_details_api_kt_example -> { + startActivity(Intent(this, DetailsApiKotlinExampleActivity::class.java)) + true + } R.id.open_japan_search_java_example -> { startActivity(Intent(this, JapanSearchJavaExampleActivity::class.java)) true diff --git a/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/api/DetailsApiKotlinExampleActivity.kt b/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/api/DetailsApiKotlinExampleActivity.kt new file mode 100644 index 000000000..3c2e97dc5 --- /dev/null +++ b/MapboxSearch/sample/src/main/java/com/mapbox/search/sample/api/DetailsApiKotlinExampleActivity.kt @@ -0,0 +1,53 @@ +package com.mapbox.search.sample.api + +import android.os.Bundle +import com.mapbox.search.AttributeSet +import com.mapbox.search.ResponseInfo +import com.mapbox.search.SearchResultCallback +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.common.AsyncOperationTask +import com.mapbox.search.details.DetailsApi +import com.mapbox.search.details.DetailsApiSettings +import com.mapbox.search.details.RetrieveDetailsOptions +import com.mapbox.search.result.SearchResult +import com.mapbox.search.sample.R + +@OptIn(ExperimentalMapboxSearchAPI::class) +class DetailsApiKotlinExampleActivity : BaseKotlinExampleActivity() { + + override val titleResId: Int = R.string.action_open_details_api_kt_example + + private lateinit var detailsApi: DetailsApi + private var task: AsyncOperationTask? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Set your Access Token here if it's not already set in some other way + // MapboxOptions.accessToken = "" + detailsApi = DetailsApi.create(DetailsApiSettings()) + } + + override fun onDestroy() { + task?.cancel() + super.onDestroy() + } + + override fun startExample() { + task = detailsApi.retrieveDetails( + mapboxId = "dXJuOm1ieHBvaTowZGY2MzE4Yi0wNGNjLTRkOTYtYTZmMy0yNmJmM2ZiODUyODU", + options = RetrieveDetailsOptions(attributeSets = AttributeSet.values().toList()), + callback = object : SearchResultCallback { + override fun onResult(result: SearchResult, responseInfo: ResponseInfo) { + logI("SearchApiExample", "Retrieve result:", result) + onFinished() + } + + override fun onError(e: Exception) { + logE("SearchApiExample", "Retrieve error", e) + onFinished() + } + } + ) + } +} diff --git a/MapboxSearch/sample/src/main/res/menu/main_activity_options_menu.xml b/MapboxSearch/sample/src/main/res/menu/main_activity_options_menu.xml index 2d41f218b..50a0e0315 100644 --- a/MapboxSearch/sample/src/main/res/menu/main_activity_options_menu.xml +++ b/MapboxSearch/sample/src/main/res/menu/main_activity_options_menu.xml @@ -94,6 +94,11 @@ android:title="@string/action_open_japan_search_kt_example" /> + + Favorites Data Provider Custom data provider Custom data provider + Details API Offline Search Along Route diff --git a/MapboxSearch/sdk/api/api-metalava.txt b/MapboxSearch/sdk/api/api-metalava.txt index 2925cea4d..4b9b5cce0 100644 --- a/MapboxSearch/sdk/api/api-metalava.txt +++ b/MapboxSearch/sdk/api/api-metalava.txt @@ -604,6 +604,50 @@ package com.mapbox.search.analytics { } +package com.mapbox.search.details { + + @com.mapbox.search.base.ExperimentalMapboxSearchAPI public interface DetailsApi { + method public default static com.mapbox.search.details.DetailsApi create(com.mapbox.search.details.DetailsApiSettings settings = DetailsApiSettings()); + method public default static final com.mapbox.search.details.DetailsApi create(); + method public default com.mapbox.search.common.AsyncOperationTask retrieveDetails(String mapboxId, com.mapbox.search.details.RetrieveDetailsOptions options, com.mapbox.search.SearchResultCallback callback); + method public com.mapbox.search.common.AsyncOperationTask retrieveDetails(String mapboxId, com.mapbox.search.details.RetrieveDetailsOptions options, java.util.concurrent.Executor executor, com.mapbox.search.SearchResultCallback callback); + field public static final com.mapbox.search.details.DetailsApi.Companion Companion; + } + + public static final class DetailsApi.Companion { + method public com.mapbox.search.details.DetailsApi create(com.mapbox.search.details.DetailsApiSettings settings = DetailsApiSettings()); + method public com.mapbox.search.details.DetailsApi create(); + } + + @com.mapbox.search.base.ExperimentalMapboxSearchAPI public final class DetailsApiSettings { + ctor public DetailsApiSettings(com.mapbox.common.location.LocationProvider? locationProvider = (), com.mapbox.search.ViewportProvider? viewportProvider = null, String? baseUrl = null); + ctor public DetailsApiSettings(com.mapbox.common.location.LocationProvider? locationProvider = (), com.mapbox.search.ViewportProvider? viewportProvider = null); + ctor public DetailsApiSettings(com.mapbox.common.location.LocationProvider? locationProvider = ()); + method public String? getBaseUrl(); + method public com.mapbox.common.location.LocationProvider? getLocationProvider(); + method public com.mapbox.search.ViewportProvider? getViewportProvider(); + property public final String? baseUrl; + property public final com.mapbox.common.location.LocationProvider? locationProvider; + property public final com.mapbox.search.ViewportProvider? viewportProvider; + } + + @com.mapbox.search.base.ExperimentalMapboxSearchAPI @kotlinx.parcelize.Parcelize public final class RetrieveDetailsOptions implements android.os.Parcelable { + ctor public RetrieveDetailsOptions(java.util.List? attributeSets = null, com.mapbox.search.common.IsoLanguageCode language = (), com.mapbox.search.common.IsoCountryCode? worldview = null); + ctor public RetrieveDetailsOptions(java.util.List? attributeSets = null, com.mapbox.search.common.IsoLanguageCode language = ()); + ctor public RetrieveDetailsOptions(java.util.List? attributeSets = null); + method public java.util.List? getAttributeSets(); + method public com.mapbox.search.common.IsoLanguageCode getLanguage(); + method public com.mapbox.search.common.IsoCountryCode? getWorldview(); + property public final java.util.List? attributeSets; + property public final com.mapbox.search.common.IsoLanguageCode language; + property public final com.mapbox.search.common.IsoCountryCode? worldview; + } + + public final class RetrieveDetailsOptionsKt { + } + +} + package com.mapbox.search.record { @kotlinx.parcelize.Parcelize public final class FavoriteRecord implements com.mapbox.search.record.IndexableRecord android.os.Parcelable { diff --git a/MapboxSearch/sdk/api/sdk.api b/MapboxSearch/sdk/api/sdk.api index a57965180..13dbac830 100644 --- a/MapboxSearch/sdk/api/sdk.api +++ b/MapboxSearch/sdk/api/sdk.api @@ -736,6 +736,63 @@ public final class com/mapbox/search/analytics/events/SearchResultsInfo$Creator public synthetic fun newArray (I)[Ljava/lang/Object; } +public abstract interface class com/mapbox/search/details/DetailsApi { + public static final field Companion Lcom/mapbox/search/details/DetailsApi$Companion; + public static fun create ()Lcom/mapbox/search/details/DetailsApi; + public static fun create (Lcom/mapbox/search/details/DetailsApiSettings;)Lcom/mapbox/search/details/DetailsApi; + public abstract fun retrieveDetails (Ljava/lang/String;Lcom/mapbox/search/details/RetrieveDetailsOptions;Lcom/mapbox/search/SearchResultCallback;)Lcom/mapbox/search/common/AsyncOperationTask; + public abstract fun retrieveDetails (Ljava/lang/String;Lcom/mapbox/search/details/RetrieveDetailsOptions;Ljava/util/concurrent/Executor;Lcom/mapbox/search/SearchResultCallback;)Lcom/mapbox/search/common/AsyncOperationTask; +} + +public final class com/mapbox/search/details/DetailsApi$Companion { + public final fun create ()Lcom/mapbox/search/details/DetailsApi; + public final fun create (Lcom/mapbox/search/details/DetailsApiSettings;)Lcom/mapbox/search/details/DetailsApi; + public static synthetic fun create$default (Lcom/mapbox/search/details/DetailsApi$Companion;Lcom/mapbox/search/details/DetailsApiSettings;ILjava/lang/Object;)Lcom/mapbox/search/details/DetailsApi; +} + +public final class com/mapbox/search/details/DetailsApi$DefaultImpls { + public static fun retrieveDetails (Lcom/mapbox/search/details/DetailsApi;Ljava/lang/String;Lcom/mapbox/search/details/RetrieveDetailsOptions;Lcom/mapbox/search/SearchResultCallback;)Lcom/mapbox/search/common/AsyncOperationTask; +} + +public final class com/mapbox/search/details/DetailsApiSettings { + public fun ()V + public fun (Lcom/mapbox/common/location/LocationProvider;)V + public fun (Lcom/mapbox/common/location/LocationProvider;Lcom/mapbox/search/ViewportProvider;)V + public fun (Lcom/mapbox/common/location/LocationProvider;Lcom/mapbox/search/ViewportProvider;Ljava/lang/String;)V + public synthetic fun (Lcom/mapbox/common/location/LocationProvider;Lcom/mapbox/search/ViewportProvider;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getBaseUrl ()Ljava/lang/String; + public final fun getLocationProvider ()Lcom/mapbox/common/location/LocationProvider; + public final fun getViewportProvider ()Lcom/mapbox/search/ViewportProvider; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/mapbox/search/details/RetrieveDetailsOptions : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun ()V + public fun (Ljava/util/List;)V + public fun (Ljava/util/List;Lcom/mapbox/search/common/IsoLanguageCode;)V + public fun (Ljava/util/List;Lcom/mapbox/search/common/IsoLanguageCode;Lcom/mapbox/search/common/IsoCountryCode;)V + public synthetic fun (Ljava/util/List;Lcom/mapbox/search/common/IsoLanguageCode;Lcom/mapbox/search/common/IsoCountryCode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getAttributeSets ()Ljava/util/List; + public final fun getLanguage ()Lcom/mapbox/search/common/IsoLanguageCode; + public final fun getWorldview ()Lcom/mapbox/search/common/IsoCountryCode; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/mapbox/search/details/RetrieveDetailsOptions$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/mapbox/search/details/RetrieveDetailsOptions; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/mapbox/search/details/RetrieveDetailsOptions; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/mapbox/search/record/FavoriteRecord : android/os/Parcelable, com/mapbox/search/record/IndexableRecord { public static final field CREATOR Landroid/os/Parcelable$Creator; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/mapbox/search/result/SearchAddress;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Lcom/mapbox/geojson/Point;Lcom/mapbox/search/result/SearchResultType;Lcom/mapbox/search/SearchResultMetadata;)V diff --git a/MapboxSearch/sdk/src/androidTest/assets/details_api/response_successful.json b/MapboxSearch/sdk/src/androidTest/assets/details_api/response_successful.json new file mode 100644 index 000000000..5ae6aba68 --- /dev/null +++ b/MapboxSearch/sdk/src/androidTest/assets/details_api/response_successful.json @@ -0,0 +1,186 @@ +{ + "geometry": { + "coordinates": [ + -77.029129, + 38.902309 + ], + "type": "Point" + }, + "properties": { + "address": "925 13th St NW", + "context": { + "address": { + "address_number": "925", + "id": "", + "name": "925 13th St NW, Washington DC, DC 20005-4005", + "street_name": "13th st nw" + }, + "country": { + "country_code": "US", + "country_code_alpha_3": "USA", + "id": "", + "name": "United States" + }, + "neighborhood": { + "id": "dXJuOm1ieHBsYzpDaXpzN0E", + "name": "Downtown" + }, + "place": { + "id": "dXJuOm1ieHBsYzpGSmlvN0E", + "name": "Washington" + }, + "postcode": { + "id": "dXJuOm1ieHBsYzpBNTd1N0E", + "name": "20005" + }, + "region": { + "id": "", + "name": "District of Columbia", + "region_code": "DC", + "region_code_full": "US-DC" + }, + "street": { + "id": "", + "name": "13th st nw" + } + }, + "coordinates": { + "latitude": 38.902309, + "longitude": -77.029129, + "routable_points": [ + { + "latitude": 38.9023073713156, + "longitude": -77.0296257487584, + "name": "POI" + } + ] + }, + "external_ids": { + "tripadvisor": "20145439" + }, + "feature_type": "poi", + "full_address": "925 13th St NW, Washington, District of Columbia 20005, United States", + "language": "en", + "maki": "marker", + "mapbox_id": "dXJuOm1ieHBvaTo0ZTg2ZWFkNS1jOWMwLTQ3OWEtOTA5Mi1kMDVlNDQ3NDdlODk", + "metadata": { + "open_hours": { + "periods": [ + { + "close": { + "day": 0, + "time": "1800" + }, + "open": { + "day": 0, + "time": "1000" + } + }, + { + "close": { + "day": 1, + "time": "1700" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "1700" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "1700" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "1700" + }, + "open": { + "day": 5, + "time": "1000" + } + }, + { + "close": { + "day": 6, + "time": "1800" + }, + "open": { + "day": 6, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM - 5:00 PM", + "Tuesday: Closed", + "Wednesday: 10:00 AM - 5:00 PM", + "Thursday: 10:00 AM - 5:00 PM", + "Friday: 10:00 AM - 5:00 PM", + "Saturday: 10:00 AM - 6:00 PM", + "Sunday: 10:00 AM - 6:00 PM" + ] + }, + "phone": "+12029313139", + "photos": [ + { + "height": 768, + "url": "https://media-cdn.tripadvisor.com/media/photo-o/2a/08/ad/cd/caption.jpg", + "width": 1024 + }, + { + "height": 1024, + "url": "https://media-cdn.tripadvisor.com/media/photo-o/2a/08/ad/fe/caption.jpg", + "width": 768 + }, + { + "height": 975, + "url": "https://media-cdn.tripadvisor.com/media/photo-o/28/b6/2d/cc/caption.jpg", + "width": 2006 + }, + { + "height": 3888, + "url": "https://media-cdn.tripadvisor.com/media/photo-o/2a/45/ec/0a/caption.jpg", + "width": 5184 + }, + { + "height": 975, + "url": "https://media-cdn.tripadvisor.com/media/photo-o/28/b6/2d/ca/caption.jpg", + "width": 2006 + } + ], + "primary_photo": "https://media-cdn.tripadvisor.com/media/photo-o/2a/08/ad/cd/caption.jpg", + "rating": 5, + "review_count": 69, + "website": "http://www.planetwordmuseum.org" + }, + "name": "Planet Word", + "operational_status": "active", + "place_formatted": "Washington, District of Columbia 20005, United States", + "poi_category": [ + "museum", + "tourist attraction" + ], + "poi_category_ids": [ + "museum", + "tourist_attraction" + ] + }, + "type": "Feature" +} \ No newline at end of file diff --git a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/SearchEngineIntegrationTest.kt b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/SearchEngineIntegrationTest.kt index 0fda778da..c6fc42957 100644 --- a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/SearchEngineIntegrationTest.kt +++ b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/SearchEngineIntegrationTest.kt @@ -1300,9 +1300,9 @@ internal class SearchEngineIntegrationTest : BaseTest() { searchEngine.retrieve(mapboxId, callback) val result = callback.getResultBlocking() - assertTrue(result is SearchResult) + assertTrue(result.isSuccess) - val searchResult = result as SearchResult + val searchResult = result.getSuccess().result assertEquals(mapboxId, searchResult.mapboxId) } @@ -1318,7 +1318,7 @@ internal class SearchEngineIntegrationTest : BaseTest() { searchEngine.retrieve(mapboxId, callback) val result = callback.getResultBlocking() - assertTrue(result is Exception) + assertTrue(result.isError) } @After diff --git a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/details/DetailsApiIntegrationTest.kt b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/details/DetailsApiIntegrationTest.kt new file mode 100644 index 000000000..e25896118 --- /dev/null +++ b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/details/DetailsApiIntegrationTest.kt @@ -0,0 +1,115 @@ +package com.mapbox.search.details + +import com.mapbox.common.MapboxOptions +import com.mapbox.geojson.Point +import com.mapbox.search.AttributeSet +import com.mapbox.search.BaseTest +import com.mapbox.search.MapboxSearchSdk +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.common.IsoCountryCode +import com.mapbox.search.common.IsoLanguageCode +import com.mapbox.search.common.SearchRequestException +import com.mapbox.search.common.metadata.OpenHours +import com.mapbox.search.tests_support.BlockingSearchResultCallback +import com.mapbox.search.utils.assertEqualsIgnoreCase +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalMapboxSearchAPI::class) +internal class DetailsApiIntegrationTest : BaseTest() { + + private lateinit var mockServer: MockWebServer + private lateinit var detailsApiSettings: DetailsApiSettings + private lateinit var detailsApi: DetailsApi + + @Before + override fun setUp() { + super.setUp() + MapboxOptions.accessToken = TEST_ACCESS_TOKEN + + mockServer = MockWebServer() + + MapboxSearchSdk.initialize( + application = targetApplication, + ) + + detailsApiSettings = DetailsApiSettings( + baseUrl = mockServer.url("").toString(), + ) + + detailsApi = DetailsApi.create(detailsApiSettings) + } + + @Test + fun testRequestParametersAreCorrect() { + mockServer.enqueue(MockResponse().setResponseCode(500)) + + val options = RetrieveDetailsOptions( + attributeSets = listOf(AttributeSet.BASIC, AttributeSet.VISIT, AttributeSet.PHOTOS, AttributeSet.VENUE), + language = IsoLanguageCode.FRENCH, + worldview = IsoCountryCode.FRANCE, + ) + + val callback = BlockingSearchResultCallback() + detailsApi.retrieveDetails(TEST_MAPBOX_ID, options, callback) + + val request = mockServer.takeRequest() + assertEqualsIgnoreCase("get", request.method!!) + + val url = request.requestUrl!! + assertEqualsIgnoreCase("//search/details/v1/retrieve/$TEST_MAPBOX_ID", url.encodedPath) + assertEquals(TEST_ACCESS_TOKEN, url.queryParameter("access_token")) + assertEquals(options.language.code, url.queryParameter("language")) + assertEquals(options.worldview!!.code, url.queryParameter("worldview")) + assertEquals( + options.attributeSets!!.joinToString(separator = ",") { it.name.lowercase() }, + url.queryParameter("attribute_sets") + ) + } + + @Test + fun testSuccessfulResponse() { + mockServer.enqueue(createSuccessfulResponse("details_api/response_successful.json")) + + val callback = BlockingSearchResultCallback() + detailsApi.retrieveDetails(TEST_MAPBOX_ID, RetrieveDetailsOptions(), callback) + + val requestResult = callback.getResultBlocking() + assertTrue(requestResult.isSuccess) + + val searchResult = requestResult.getSuccess().result + + assertEquals("Planet Word", searchResult.name) + assertEquals( + "dXJuOm1ieHBvaTo0ZTg2ZWFkNS1jOWMwLTQ3OWEtOTA5Mi1kMDVlNDQ3NDdlODk", + searchResult.mapboxId, + ) + assertEquals(Point.fromLngLat(-77.029129, 38.902309), searchResult.coordinate) + assertTrue(searchResult.metadata?.openHours is OpenHours.Scheduled) + assertEquals("+12029313139", searchResult.metadata?.phone) + assertEquals(5, searchResult.metadata?.otherPhotos?.size) + } + + @Test + fun testErrorResponse() { + mockServer.enqueue(MockResponse().setResponseCode(404)) + + val callback = BlockingSearchResultCallback() + detailsApi.retrieveDetails(TEST_MAPBOX_ID, RetrieveDetailsOptions(), callback) + + val requestResult = callback.getResultBlocking() + assertTrue(requestResult.isError) + + val e = requestResult.getError().e + assertTrue(e is SearchRequestException && e.code == 404) + } + + private companion object { + const val TEST_ACCESS_TOKEN = "pk.test" + const val TEST_MAPBOX_ID = "test-id" + } +} diff --git a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/ReverseGeocodingSearchIntegrationTest.kt b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/ReverseGeocodingSearchIntegrationTest.kt index cefb0c238..8ee86122f 100644 --- a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/ReverseGeocodingSearchIntegrationTest.kt +++ b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/ReverseGeocodingSearchIntegrationTest.kt @@ -208,8 +208,8 @@ internal class ReverseGeocodingSearchIntegrationTest : BaseTest() { openHours = OpenHours.Scheduled( periods = listOf( OpenPeriod( - open = WeekTimestamp(WeekDay.MONDAY, 9, 0), - closed = WeekTimestamp(WeekDay.MONDAY, 23, 45) + open = WeekTimestamp(WeekDay.SUNDAY, 9, 0), + closed = WeekTimestamp(WeekDay.SUNDAY, 23, 45) ) ) ), diff --git a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/SearchEngineIntegrationTest.kt b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/SearchEngineIntegrationTest.kt index 460e6767d..de31934e6 100644 --- a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/SearchEngineIntegrationTest.kt +++ b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/search_box/SearchEngineIntegrationTest.kt @@ -661,8 +661,8 @@ internal class SearchEngineIntegrationTest : BaseTest() { OpenHours.Scheduled( periods = listOf( OpenPeriod( - open = WeekTimestamp(WeekDay.SATURDAY, 7, 0), - closed = WeekTimestamp(WeekDay.SATURDAY, 20, 0) + open = WeekTimestamp(WeekDay.FRIDAY, 7, 0), + closed = WeekTimestamp(WeekDay.FRIDAY, 20, 0) ) ) ), openHours @@ -957,7 +957,7 @@ internal class SearchEngineIntegrationTest : BaseTest() { searchEngine.retrieve(mapboxId, callback) val result = callback.getResultBlocking() - assertTrue(result is Exception) + assertTrue(result.isError) } @Test diff --git a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/tests_support/BlockingSearchResultCallback.kt b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/tests_support/BlockingSearchResultCallback.kt index b362ec354..2d4359a4b 100644 --- a/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/tests_support/BlockingSearchResultCallback.kt +++ b/MapboxSearch/sdk/src/androidTest/java/com/mapbox/search/tests_support/BlockingSearchResultCallback.kt @@ -5,13 +5,30 @@ import com.mapbox.search.SearchResultCallback import com.mapbox.search.common.tests.BaseBlockingCallback import com.mapbox.search.result.SearchResult -internal class BlockingSearchResultCallback : SearchResultCallback, BaseBlockingCallback() { +internal class BlockingSearchResultCallback : + SearchResultCallback, + BaseBlockingCallback() { override fun onResult(result: SearchResult, responseInfo: ResponseInfo) { - publishResult(result) + publishResult(Result.Success(result, responseInfo)) } override fun onError(e: Exception) { - publishResult(e) + publishResult(Result.Error(e)) + } + + sealed class Result { + + val isSuccess: Boolean + get() = this is Success + + val isError: Boolean + get() = !isSuccess + + fun getSuccess() = this as Success + fun getError() = this as Error + + data class Success(val result: SearchResult, val responseInfo: ResponseInfo) : Result() + data class Error(val e: Exception) : Result() } } diff --git a/MapboxSearch/sdk/src/main/java/com/mapbox/search/SearchEngineFactory.kt b/MapboxSearch/sdk/src/main/java/com/mapbox/search/SearchEngineFactory.kt index e8f0a3f9b..da48b8ec4 100644 --- a/MapboxSearch/sdk/src/main/java/com/mapbox/search/SearchEngineFactory.kt +++ b/MapboxSearch/sdk/src/main/java/com/mapbox/search/SearchEngineFactory.kt @@ -1,5 +1,6 @@ package com.mapbox.search +import com.mapbox.common.location.LocationProvider import com.mapbox.search.analytics.AnalyticsService import com.mapbox.search.base.BaseSearchSdkInitializerImpl import com.mapbox.search.base.StubCompletionCallback @@ -93,18 +94,32 @@ internal class SearchEngineFactory { else -> null } + return createCoreEngineByApiType( + apiType = apiType, + baseUrl = baseUrl, + locationProvider = settings.locationProvider, + viewportProvider = settings.viewportProvider, + ) + } + + fun createCoreEngineByApiType( + apiType: ApiType, + baseUrl: String?, + locationProvider: LocationProvider?, + viewportProvider: ViewportProvider?, + ): CoreSearchEngineInterface { // Workaround for sync location provider in test environment. // Needed while https://github.com/mapbox/mapbox-search-sdk/issues/671 not fixed - val coreLocationProvider = if (settings.locationProvider is CoreLocationProvider) { - settings.locationProvider + val coreLocationProvider = if (locationProvider is CoreLocationProvider) { + locationProvider } else { WrapperLocationProvider( LocationEngineAdapter( BaseSearchSdkInitializerImpl.app, - settings.locationProvider + locationProvider ) ) { - settings.viewportProvider?.getViewport()?.mapToCore() + viewportProvider?.getViewport()?.mapToCore() } } diff --git a/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApi.kt b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApi.kt new file mode 100644 index 000000000..315491248 --- /dev/null +++ b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApi.kt @@ -0,0 +1,94 @@ +package com.mapbox.search.details + +import com.mapbox.search.ApiType +import com.mapbox.search.MapboxSearchSdk +import com.mapbox.search.SearchEngineFactory +import com.mapbox.search.SearchResultCallback +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.base.core.getUserActivityReporter +import com.mapbox.search.common.AsyncOperationTask +import com.mapbox.search.common.concurrent.SearchSdkMainThreadWorker +import java.util.concurrent.Executor + +/** + * The Details API provides access to POI metadata, boundary details, addresses and places. + * For more information, visit [Details API page](https://docs.mapbox.com/api/search/details/). + * + * Instance of the [DetailsApi] can be obtained with [DetailsApi.create]. + */ +@ExperimentalMapboxSearchAPI +public interface DetailsApi { + + /** + * Request basic metadata for a POI, which includes attributes such as POI name, address, + * coordinates, primary photo and category classification. + * + * To retrieve additional attributes beyond the basic data for a POI, + * specify [RetrieveDetailsOptions.attributeSets] in the provided [options]. + * + * @param mapboxId A unique identifier for the geographic feature. + * @param options Retrieve options. + * @param callback Search result callback. Events are dispatched on the main thread. + * @return [AsyncOperationTask] object representing pending completion of the request + */ + public fun retrieveDetails( + mapboxId: String, + options: RetrieveDetailsOptions, + callback: SearchResultCallback, + ): AsyncOperationTask = retrieveDetails( + mapboxId = mapboxId, + options = options, + executor = SearchSdkMainThreadWorker.mainExecutor, + callback = callback, + ) + + /** + * Request basic metadata for a POI, which includes attributes such as POI name, address, + * coordinates, primary photo and category classification. + * + * To retrieve additional attributes beyond the basic data for a POI, + * specify [RetrieveDetailsOptions.attributeSets] in the provided [options]. + * + * @param mapboxId A unique identifier for the geographic feature. + * @param options Retrieve options. + * @param executor [Executor] used for events dispatching, default is the main thread. + * @param callback Search result callback. + * @return [AsyncOperationTask] object representing pending completion of the request + */ + public fun retrieveDetails( + mapboxId: String, + options: RetrieveDetailsOptions, + executor: Executor, + callback: SearchResultCallback, + ): AsyncOperationTask + + /** + * Companion object. + */ + public companion object { + + /** + * Creates a new instance of the [DetailsApi]. + * + * @param settings [DetailsApiSettings] settings. + * @return a new instance instance of the [DetailsApi]. + */ + @JvmStatic + @JvmOverloads + public fun create(settings: DetailsApiSettings = DetailsApiSettings()): DetailsApi { + val coreEngine = SearchEngineFactory().createCoreEngineByApiType( + apiType = ApiType.SEARCH_BOX, + baseUrl = settings.baseUrl, + locationProvider = settings.locationProvider, + viewportProvider = settings.viewportProvider, + ) + + return DetailsApiImpl( + coreEngine, + getUserActivityReporter(), + MapboxSearchSdk.searchRequestContextProvider, + MapboxSearchSdk.searchResultFactory, + ) + } + } +} diff --git a/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiImpl.kt b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiImpl.kt new file mode 100644 index 000000000..80ae5a63b --- /dev/null +++ b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiImpl.kt @@ -0,0 +1,62 @@ +package com.mapbox.search.details + +import com.mapbox.search.SearchResultCallback +import com.mapbox.search.adapter.SearchResultCallbackAdapter +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.base.SearchRequestContextProvider +import com.mapbox.search.base.core.CoreApiType +import com.mapbox.search.base.core.CoreSearchEngineInterface +import com.mapbox.search.base.engine.BaseSearchEngine +import com.mapbox.search.base.engine.OneStepRequestCallbackWrapper +import com.mapbox.search.base.result.SearchResultFactory +import com.mapbox.search.common.AsyncOperationTask +import com.mapbox.search.internal.bindgen.UserActivityReporterInterface +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@OptIn(ExperimentalMapboxSearchAPI::class) +internal class DetailsApiImpl( + private val coreEngine: CoreSearchEngineInterface, + private val activityReporter: UserActivityReporterInterface, + private val requestContextProvider: SearchRequestContextProvider, + private val searchResultFactory: SearchResultFactory, + private val engineExecutorService: ExecutorService = DEFAULT_EXECUTOR, +) : BaseSearchEngine(), DetailsApi { + + override fun retrieveDetails( + mapboxId: String, + options: RetrieveDetailsOptions, + executor: Executor, + callback: SearchResultCallback + ): AsyncOperationTask { + activityReporter.reportActivity("details-api-retrieve") + + val baseCallback = SearchResultCallbackAdapter(callback) + + return makeRequest(baseCallback) { task -> + val requestId = coreEngine.retrieveDetails( + mapboxId, + options.mapToCore(), + OneStepRequestCallbackWrapper( + searchResultFactory = searchResultFactory, + callbackExecutor = executor, + workerExecutor = engineExecutorService, + searchRequestTask = task, + searchRequestContext = requestContextProvider.provide(CoreApiType.SEARCH_BOX), + isOffline = false, + ) + ) + + task.addOnCancelledCallback { + coreEngine.cancel(requestId) + } + } + } + + private companion object { + val DEFAULT_EXECUTOR: ExecutorService = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "DetailsApi executor") + } + } +} diff --git a/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiSettings.kt b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiSettings.kt new file mode 100644 index 000000000..ed2728237 --- /dev/null +++ b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/DetailsApiSettings.kt @@ -0,0 +1,72 @@ +package com.mapbox.search.details + +import android.Manifest +import com.mapbox.common.location.LocationProvider +import com.mapbox.common.location.LocationServiceFactory +import com.mapbox.search.ViewportProvider +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.base.location.defaultLocationProvider + +/** + * Settings used for [DetailsApi] configuration. + * @see DetailsApi + */ +@ExperimentalMapboxSearchAPI +public class DetailsApiSettings @JvmOverloads constructor( + + /** + * The mechanism responsible for providing location approximations to the SDK. + * By default [LocationProvider] is provided by [LocationServiceFactory]. + * Note that this class requires [Manifest.permission.ACCESS_COARSE_LOCATION] or + * [Manifest.permission.ACCESS_FINE_LOCATION] to work properly. + */ + public val locationProvider: LocationProvider? = defaultLocationProvider(), + + /** + * Viewport provider instance. + */ + public val viewportProvider: ViewportProvider? = null, + + /** + * Base endpoint URL. + */ + public val baseUrl: String? = null, +) { + + /** + * @suppress + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DetailsApiSettings + + if (locationProvider != other.locationProvider) return false + if (viewportProvider != other.viewportProvider) return false + if (baseUrl != other.baseUrl) return false + + return true + } + + /** + * @suppress + */ + override fun hashCode(): Int { + var result = locationProvider?.hashCode() ?: 0 + result = 31 * result + (viewportProvider?.hashCode() ?: 0) + result = 31 * result + (baseUrl?.hashCode() ?: 0) + return result + } + + /** + * @suppress + */ + override fun toString(): String { + return "DetailsApiSettings(" + + "locationProvider=$locationProvider, " + + "viewportProvider=$viewportProvider, " + + "baseUrl=$baseUrl" + + ")" + } +} diff --git a/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/RetrieveDetailsOptions.kt b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/RetrieveDetailsOptions.kt new file mode 100644 index 000000000..642aed47a --- /dev/null +++ b/MapboxSearch/sdk/src/main/java/com/mapbox/search/details/RetrieveDetailsOptions.kt @@ -0,0 +1,96 @@ +package com.mapbox.search.details + +import android.os.Parcelable +import com.mapbox.search.AttributeSet +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.base.defaultLocaleLanguage +import com.mapbox.search.common.IsoCountryCode +import com.mapbox.search.common.IsoLanguageCode +import com.mapbox.search.internal.bindgen.DetailsOptions +import com.mapbox.search.mapToCore +import kotlinx.parcelize.Parcelize + +/** + * Options, used for the [DetailsApi.retrieveDetails]. + */ +@ExperimentalMapboxSearchAPI +@Parcelize +public class RetrieveDetailsOptions @JvmOverloads constructor( + + /** + * Besides the basic metadata attributes, developers can request additional + * attributes by setting attribute_sets parameter with attribute set values, + * for example &attribute_sets=basic,photos,visit. + * The requested metadata will be provided in metadata object in the response. + */ + public val attributeSets: List? = null, + + /** + * Specify the user’s language. This parameter controls the language of the text supplied in responses. + * If language is not set explicitly, then language from default system locale will be used. + */ + public val language: IsoLanguageCode = defaultLocaleLanguage(), + + /** + * The ISO country code to requests a worldview for the location data, + * if applicable data is available. + * This parameters will only be applicable for Boundaries and Places feature types. + */ + public val worldview: IsoCountryCode? = null, +) : Parcelable { + + /** + * @suppress + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RetrieveDetailsOptions + + if (attributeSets != other.attributeSets) return false + if (language != other.language) return false + if (worldview != other.worldview) return false + + return true + } + + /** + * @suppress + */ + override fun hashCode(): Int { + var result = attributeSets?.hashCode() ?: 0 + result = 31 * result + language.hashCode() + result = 31 * result + (worldview?.hashCode() ?: 0) + return result + } + + /** + * @suppress + */ + override fun toString(): String { + return "RetrieveDetailsOptions(" + + "attributeSets=$attributeSets, " + + "language=$language, " + + "worldview=$worldview" + + ")" + } +} + +@OptIn(ExperimentalMapboxSearchAPI::class) +@JvmSynthetic +internal fun RetrieveDetailsOptions.mapToCore(): DetailsOptions { + return DetailsOptions( + attributeSets?.fixedAttributesOption()?.map { it.mapToCore() }, + language.code, + worldview?.code, + ) +} + +private fun List.fixedAttributesOption(): List { + return if (isNotEmpty() && !contains(AttributeSet.BASIC)) { + this + AttributeSet.BASIC + } else { + this + } +} diff --git a/MapboxSearch/sdk/src/test/java/com/mapbox/search/details/RetrieveDetailsOptionsTest.kt b/MapboxSearch/sdk/src/test/java/com/mapbox/search/details/RetrieveDetailsOptionsTest.kt new file mode 100644 index 000000000..8d66f3494 --- /dev/null +++ b/MapboxSearch/sdk/src/test/java/com/mapbox/search/details/RetrieveDetailsOptionsTest.kt @@ -0,0 +1,41 @@ +package com.mapbox.search.details + +import com.mapbox.search.AttributeSet +import com.mapbox.search.base.ExperimentalMapboxSearchAPI +import com.mapbox.search.base.core.CoreAttributeSet +import com.mapbox.search.common.IsoCountryCode +import com.mapbox.search.common.IsoLanguageCode +import com.mapbox.search.common.tests.ToStringVerifier +import nl.jqno.equalsverifier.EqualsVerifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalMapboxSearchAPI::class) +internal class RetrieveDetailsOptionsTest { + + @Test + fun `Test generated equals(), hashCode() and toString() methods`() { + EqualsVerifier.forClass(RetrieveDetailsOptions::class.java) + .verify() + + ToStringVerifier(RetrieveDetailsOptions::class).verify() + } + + @Test + fun `Test mapToCore() function`() { + val options = RetrieveDetailsOptions( + attributeSets = listOf(AttributeSet.BASIC, AttributeSet.VISIT), + language = IsoLanguageCode.FRENCH, + worldview = IsoCountryCode.FRANCE, + ) + + val coreOptions = options.mapToCore() + + assertEquals( + listOf(CoreAttributeSet.BASIC, CoreAttributeSet.VISIT), + coreOptions.attributeSets + ) + assertEquals(IsoLanguageCode.FRENCH.code, coreOptions.language) + assertEquals(IsoCountryCode.FRANCE.code, coreOptions.worldview) + } +}