From 93ff681a7a7e3b7aae69f2803687be60e3e4de94 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Thu, 20 Sep 2018 19:36:15 -0700 Subject: [PATCH 01/17] Update README.md --- README.md | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 80d9df66..d3a2d983 100755 --- a/README.md +++ b/README.md @@ -1,34 +1,14 @@ -# Constructor.io Android Client Library +[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) +# Constructor.io Android Client Library -An android client library for constructor.io suggestions engine +An Android Client library for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). # Usage -## 1a. Install using gradle -Add following to main gradle file: - -``` -allprojects { - repositories { - ... - maven { url 'https://jitpack.io' } - } -} -``` - -and then in your app gradle file include Constructor.IO dependency: - -``` -dependencies { - implementation 'com.github.Constructor-io:constructorio-client-android:v0.1' -} -``` - -## 1b. Manual import +## 1. Install -* Clone the android client repository from github ```git clone https://github.com/Constructor-io/constructorio-client-android.git``` -* Open and build the project in your IDE +Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.0.0) to add the client to your project. ## 2. Retrieve an autocomplete key From 395c38066359ebe455df15d2c668641d7b9c8e6a Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Thu, 27 Sep 2018 16:44:01 -0700 Subject: [PATCH 02/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d3a2d983..a8ab786c 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) +[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) ![Android min](https://img.shields.io/badge/Android-4.4%2B-green.svg) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) # Constructor.io Android Client Library From a7170866a622bacb4369f8f6c6454e78d8f03bd1 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Wed, 3 Oct 2018 13:47:50 -0700 Subject: [PATCH 03/17] Ch2950/documentation (#19) * Documentation update --- README.md | 324 +++++++++++++++++++----------------------------------- 1 file changed, 112 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index a8ab786c..c2c0ded4 100755 --- a/README.md +++ b/README.md @@ -1,253 +1,153 @@ [![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) ![Android min](https://img.shields.io/badge/Android-4.4%2B-green.svg) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) -# Constructor.io Android Client Library +# Constructor.io Android Client -An Android Client library for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). - -# Usage +An Android Client for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). ## 1. Install Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.0.0) to add the client to your project. -## 2. Retrieve an autocomplete key - -You can find this in your [Constructor.io dashboard](https://constructor.io/dashboard). - -Contact sales if you'd like to sign up, or support if you believe your company already has an account. +## 2. Retrieve an API key +You can find this in your [Constructor.io dashboard](https://constructor.io/dashboard). Contact sales if you'd like to sign up, or support if you believe your company already has an account. -## 3. Init the Constructor.io Library +## 3. Implement the Autocomplete UI In your Application class add the following code with your key: +```kotlin +override fun onCreate() { + super.onCreate() + ConstructorIo.init(this, "YOUR API KEY") + + val fragment = supportFragmentManager.findFragmentById(R.id.fragment_suggestions) as SuggestionsFragment + fragment.setConstructorListener(object : ConstructorListener { + override fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) { + Log.d(TAG, "onSuggestionSelected") + } + + override fun onQuerySentToServer(query: String) { + Log.d(TAG, "onQuerySentToServer") + } + + override fun onSuggestionsRetrieved(suggestions: List) { + Log.d(TAG, "onSuggestionsRetrieved") + } + + override fun onErrorGettingSuggestions(error: Throwable) { + Log.d(TAG, "handle network error getting suggestion") + } + }) +} ``` - override fun onCreate() { - super.onCreate() - ConstructorIo.init(this, "your-key") - } -``` -## 4a. Use default out-of-the-box UI - -To use the default, out-of-the-box UI, add the Sample Suggestions Fragment to your layout: - -```xml - -``` - -Skip to #5 if not customizing UI - -## 4b. Customize UI - -### Extend the suggestion screen fragment `BaseSuggestionFragment`. - -Implement the following: - -|Element name|Returned Type|Description| -|--|--|--| -|`layoutId`|`Int`|Returns your custom layout resource id for the fragment.| -|`getSuggestionsInputId`|`EditText`|Returns the id of the suggestion input field.| -|`getSuggestionListId`|`RecyclerView`|Returns your suggestion list id.| -|`getSuggestionAdapter`|Class that extends from `BaseSuggestionAdapter`|Returns your custom adapter.| -|`getProgressId`|Int`|Returns progress indicator id, used when request is being in progress. Return 0 for no progress| - -To see an example of usage, you can look at `SampleActivityCustom`. - -### Extend the suggestion items adapter `BaseSuggestionsAdapter`. - -Implement the following : - -|Element name|Returned Type|Description| -|--|--|--| -|`getItemLayoutId()`|`Int`|Returns your custom adapter item layout id for suggestion.| -|`getSuggestionNameId()`|`Int`|Return your text view id - the text will be the suggestion. | -|`getSuggestionGroupNameId()`|`Int`|Return your text view id - the text will be the suggestion group name, if found. | -|`onViewTypeSuggestion`|`String` text| Triggered when inflating an item which is a suggestion. Read below for more info.| -|`styleHighlightedSpans(spannable: Spannable, spanStart: Int, spanEnd: Int)`|`Unit`| Override to apply custom styling to highlighted part of suggestions search result. `spannable` is highlighted part of suggestion name, start and end mark position of the `spannable` within whole text.| - -abstract val styleHighlightedSpans: ((spannable: Spannable, spanStart: Int, spanEnd: Int) -> Unit)? - -In case you need to modify something in the ViewHolder (e.g make the group name bold) you can get a reference to it using `getHolder()` - -To see an example of usage, you can look at `SampleActivityCustom`. - -## 5. Get a reference to the `SuggestionsFragment` and add `ConstructorListener`: - -``` - val fragment = supportFragmentManager.findFragmentById(R.id.fragment_suggestions) as SuggestionsFragment - fragment.setConstructorListener(object : ConstructorListener { - override fun onSuggestionsRetrieved(suggestions: List) { - //got suggestions for a query - } - - override fun onQuerySentToServer(query: String) { - //request being made to server - } - - override fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) { - //called when user taps on suggestion - } - - override fun onErrorGettingSuggestions(error: Throwable) { - //called when there is error getting suggestions - } - }) -``` -# Additional references -## Searching in Groups -Any data value can belong to a group. We will show the group name right below the item itself, if available. - -Let's remember the selection event. - -``` -fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) -``` +### Selecting Results +To respond to a user selecting an autocomplete result, use the `onSuggestionSelected` method. If the autocomplete result has both a suggested term to search for and a group to search within (as in Apples in Juice Drinks), the group will be passed into the method. -Note `group` of type Group. It represents the group for an item and includes the following parameters: +### Performing Searches +To respond to a user performing a search (instead of selecting an autocomplete result), use the `onQuerySentToServer` method. -|Type|Name|Description| -|--|--|--| -|String|groupId|The group's id.| -|String|displayName|The group's display name.| -|String|path|The path to get more data for the group.| +## 4. Customize the Autocomplete UI -Let's say you search for 'apple' and the results are: +### Using the Default UI +To use the default, out-of-the-box UI, add the Sample Suggestions Fragment to your layout: +```xml + ``` -"suggestions": [ - { - "data": { - "groups": [ - { - "display_name": "food", - "group_id": "12", - "path": "/0/222/344" - }, - { - "display_name": "gadgets", - "group_id": "34", - "path": "/0/252/346/350" - } - ] - }, - "value": "apple" - } -] -``` - -We received two groups (food and gadgets) for our suggestion (apple). This means we'll have two suggestions in total: -1. 'apple' in group 'food' -2. 'apple' in group 'gadgets' -When the user taps on (1), term will be `apple` and group name will be `food`. +### Using a Custom UI -When the user taps on (2), term will be `apple` and group name will be `gadgets`. +To fully customize the UI, extend the `BaseSuggestionFragment` and the `BaseSuggestionsAdapter` -In other words, you can simply check whether the group property is null to find out if the user tapped on a search-in-group result: +```kotlin +class CustomSearchFragment : BaseSuggestionFragment() { -``` -fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String) { - if (group == null) { - // user tapped on an item - - } else { - // user tapped on a group + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + view?.backButton?.setOnClickListener { + view?.input?.text?.clear() + clearSuggestions() } + view?.searchButton?.setOnClickListener { triggerSearch() } } -``` - -## ConstructorListener Interface - -### onQuerySentToServer - -`onQuerySentToServer(query: String)` - -Triggered when the query is sent to the server. - -Parameter|Type|Description -|--|--|--| -`query`|String|The query made by the user. - -### onSuggestionSelected - -`onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?)` - -Triggered when a suggestion is selected. - -|Parameter|Type|Description| -|--|--|--| -|`term`|String|The suggestion selected.| -|`group`|Group|Provides data on the group the selected term belongs to. Otherwise null.| -|`autocompleteSection`|String|The autocomplete section to which the selected term belongs (e.g "Search Suggestions", "Products"...)| -### onSuggestionsRetrieved -`onSuggestionsRetrieved(suggestions: List)` - -Triggered when the results for the query in question is retrieved. - -`suggestions` is a list of `Suggestion`s with the following parameters: - -|Parameter|Type|Description| -|--|--|--| -|`text`|String|The name of the suggestion.| -|`groups`|List|The top groups containing items that match for the query.| -|`matchedTerms`|List|matched terms within the query| -|`sectionName`|String|name of the section eg. "Search Suggestions", "Products"| - -### onErrorGettingSuggestions -`override fun onErrorGettingSuggestions(error: Throwable)` - -Triggered when error occured while requesting suggestions. - -|Parameter|Type|Description| -|--|--|--| -|`error`|Throwable|Exception thrown.| - -## BaseSuggestionFragment Abstract Class - -Default fragment expose two additional methods for easier implementing custom UI: - -### trackSearch() + // Return your custom adapter + override fun getSuggestionAdapter(): BaseSuggestionsAdapter { + return CustomSuggestionsAdapter() + } -Manually track search using text in the input box. + // Return your id of the suggestion input field + override fun getSuggestionsInputId(): Int { + return R.id.input + } -### clearSuggestions() + // Return your id of the suggestion list + override fun getSuggestionListId(): Int { + return R.id.suggestions + } -Clear input box and suggestion list. + // Return your progress indicator id, used when request is being in progress. Return 0 for no progress + override fun getProgressId(): Int { + return 0 + } + + // Return your custom layout resource id for the fragment + override fun layoutId(): Int { + return R.layout.fragment_custom_suggestions + } -## ConstructorIO public API +} -### trackConversion(term: String, itemId: String, revenue: String?) +class CustomSuggestionsAdapter() : BaseSuggestionsAdapter() { + + // Triggered when inflating an item which is a suggestion. + override fun onViewTypeSuggestion(holder: ViewHolder, suggestion: String, highlightedSuggestion: Spannable, groupName: String?) { + holder.suggestionName.text = highlightedSuggestion + val spans = highlightedSuggestion.getSpans(0, highlightedSuggestion.length, StyleSpan::class.java) + spans.forEach { highlightedSuggestion.setSpan(ForegroundColorSpan(Color.parseColor("#222222")), highlightedSuggestion.getSpanStart(it), highlightedSuggestion.getSpanEnd(it), 0) } + groupName?.let { holder.suggestionGroupName.text = holder.suggestionGroupName.context.getString(R.string.suggestion_group, it) } + } -Track conversion event + // Return your custom adapter item layout id for suggestion + override val itemLayoutId: Int + get() = R.layout.item_suggestion + + // Return your text view id - the text will be the suggestion. + override val suggestionNameId: Int + get() = R.id.suggestionName + + // Return your text view id - the text will be the suggestion group name, if present + override val suggestionGroupNameId: Int + get() = R.id.suggestionGroupName + +} +``` -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Optional term for which tracking event is reported.| -|`itemId`|String|Id of item for which we want to trigger an event.| -|`revenue`|String|Optional revenue indicator.| +## 5. Instrument Behavioral Events -### trackSearchResultClickThrough(term: String, itemId: String, position: String?) +The Android Client sends behavioral events to [Constructor.io](http://constructor.io/) in order to continuously learn and improve results for future Autosuggest and Search requests. The Client only sends events in response to being called by the consuming app or in response to user interaction . For example, if the consuming app never calls the SDK code, no events will be sent. Besides the explicitly passed in event parameters, all user events contain a GUID based user ID that the client sets to identify the user as well as a session ID. -Track search result click event +Three types of these events exist: -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Term used for search.| -|`itemId`|String|Id of item for which we want to track an event.| -|`position`|String|Optional position of clicked item on the list.| +1. **General Events** are sent as needed when an instance of the Client is created or initialized +1. **Autocomplete Events** measure user interaction with autocomplete results and the `CIOAutocompleteViewController` sends them automatically. +1. **Search Events** measure user interaction with search results and the consuming app has to explicitly instrument them itself -### triggerSearchResultLoadedEvent(term: String, resultCount: Int) +```kotlin +import io.constructor.core.ConstructorIo -Track search results loaded event +// Track search results loaded (term, resultCount) +ConstructorIo.trackSearchResultLoaded("a search term", 123) -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Term used for search.| -|`resultCount`|Int|Number of items found.| +// Track search result click (term, itemId, position) +ConstructorIo.trackSearchResultClickThrough("a search term", "an item id", "1") +// Track conversion (item id, term, revenue) +constructorIO.trackConversion("an item id", "a search term", "45.00") +``` From 7401340a8ec11e08ae5be6b422bdb58050fe32c6 Mon Sep 17 00:00:00 2001 From: qbasso Date: Mon, 22 Oct 2018 17:00:52 +0200 Subject: [PATCH 04/17] added sections number of results config parameters (#20) * added sections number of results config parameters * Fixed a few leftover references --- .../java/io/constructor/core/Constants.kt | 2 ++ .../java/io/constructor/core/ConstructorIo.kt | 19 +++++++++++-------- .../data/interceptor/TokenInterceptor.kt | 9 ++++++--- ...lMemoryHolder.kt => ConfigMemoryHolder.kt} | 3 ++- .../injection/component/AppComponent.kt | 4 ++-- .../constructor/injection/module/AppModule.kt | 6 +++--- .../injection/module/NetworkModule.kt | 4 ++-- .../io/constructor/core/ConstructorIoTest.kt | 15 ++++++++------- ...olderTest.kt => ConfigMemoryHolderTest.kt} | 10 +++++----- 9 files changed, 41 insertions(+), 31 deletions(-) rename library/src/main/java/io/constructor/data/memory/{TestCellMemoryHolder.kt => ConfigMemoryHolder.kt} (90%) rename library/src/test/java/io/constructor/data/memory/{TestCellMemoryHolderTest.kt => ConfigMemoryHolderTest.kt} (60%) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 71aae050..6a4ce316 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -21,6 +21,7 @@ class Constants { const val CLIENT = "c" const val EVENT = "tr" const val AUTOCOMPLETE_KEY = "autocomplete_key" + const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" } @@ -30,6 +31,7 @@ class Constants { const val EVENT_SEARCH = "search" const val EVENT_SESSION_START = "session_start" const val SEARCH_SUGGESTIONS = "Search Suggestions" + const val PRODUCTS = "Products" const val EVENT_SEARCH_RESULTS = "search-results" const val EVENT_INPUT_FOCUS = "focus" } diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 4db42f34..605c8e41 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -5,7 +5,7 @@ import android.content.Context import io.constructor.BuildConfig import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent @@ -26,7 +26,7 @@ object ConstructorIo { private lateinit var dataManager: DataManager private lateinit var preferenceHelper: PreferencesHelper - private lateinit var testCellMemoryHolder: TestCellMemoryHolder + private lateinit var configMemoryHolder: ConfigMemoryHolder private lateinit var context: Context private var disposable = CompositeDisposable() @@ -49,15 +49,18 @@ object ConstructorIo { })) } - fun init(context: Context?, apiKey: String, defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { + fun init(context: Context?, apiKey: String, autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, + Constants.QueryValues.PRODUCTS to 0), defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { if (context == null) { throw IllegalStateException("context is null, please init library using ConstructorIo.with(context)") } this.context = context.applicationContext dataManager = component.dataManager() preferenceHelper = component.preferenceHelper() - testCellMemoryHolder = component.testCellMemoryHolder() + configMemoryHolder = component.configMemoryHolder() + configMemoryHolder.autocompleteResultCount = autocompleteResultCount preferenceHelper.token = apiKey + preferenceHelper.defaultItemSection = defaultItemSection if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() @@ -69,21 +72,21 @@ object ConstructorIo { fun getClientId() = preferenceHelper.id fun setTestCellValues(pair1: Pair, pair2: Pair? = null, pair3: Pair? = null) { - testCellMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) + configMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) } fun clearTestCellValues() { - testCellMemoryHolder.testCellParams = emptyList() + configMemoryHolder.testCellParams = emptyList() } - internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, testCellMemoryHolder: TestCellMemoryHolder) { + internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { if (context == null) { throw IllegalStateException("Context is null, please init library using ConstructorIo.with(context)") } this.context = context.applicationContext this.dataManager = dataManager this.preferenceHelper = preferenceHelper - this.testCellMemoryHolder = testCellMemoryHolder + this.configMemoryHolder = configMemoryHolder preferenceHelper.token = apiKey if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index 42483aaa..584c02e1 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -4,12 +4,12 @@ import android.content.Context import io.constructor.BuildConfig import io.constructor.core.Constants import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import okhttp3.Interceptor import okhttp3.Response -class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val testCellMemoryHolder: TestCellMemoryHolder) : Interceptor { +class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val configMemoryHolder: ConfigMemoryHolder) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() @@ -18,11 +18,14 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - testCellMemoryHolder.testCellParams.forEach { + configMemoryHolder.testCellParams.forEach { it?.let { builder.addQueryParameter(it.first, it.second) } } + configMemoryHolder.autocompleteResultCount?.entries?.forEach { + builder.addQueryParameter(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString()) + } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) diff --git a/library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt similarity index 90% rename from library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt rename to library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt index 481a9b93..71f516d1 100644 --- a/library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt +++ b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt @@ -4,7 +4,7 @@ import io.constructor.util.base64Decode import io.constructor.util.base64Encode import javax.inject.Inject -class TestCellMemoryHolder @Inject constructor() { +class ConfigMemoryHolder @Inject constructor() { private var backingString = "" @@ -32,4 +32,5 @@ class TestCellMemoryHolder @Inject constructor() { backingString = combined } + var autocompleteResultCount: Map? = null } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/injection/component/AppComponent.kt b/library/src/main/java/io/constructor/injection/component/AppComponent.kt index 78d715b0..e0b0be1b 100755 --- a/library/src/main/java/io/constructor/injection/component/AppComponent.kt +++ b/library/src/main/java/io/constructor/injection/component/AppComponent.kt @@ -4,7 +4,7 @@ import android.content.Context import dagger.Component import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.remote.ConstructorApi import io.constructor.injection.ApplicationContext import io.constructor.injection.module.AppModule @@ -21,7 +21,7 @@ interface AppComponent { fun preferenceHelper(): PreferencesHelper - fun testCellMemoryHolder(): TestCellMemoryHolder + fun configMemoryHolder(): ConfigMemoryHolder fun constructorApi(): ConstructorApi } diff --git a/library/src/main/java/io/constructor/injection/module/AppModule.kt b/library/src/main/java/io/constructor/injection/module/AppModule.kt index 59765ab2..c48983ca 100755 --- a/library/src/main/java/io/constructor/injection/module/AppModule.kt +++ b/library/src/main/java/io/constructor/injection/module/AppModule.kt @@ -4,7 +4,7 @@ import android.content.Context import dagger.Module import dagger.Provides import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.injection.ApplicationContext import javax.inject.Singleton @@ -24,7 +24,7 @@ class AppModule(private val application: Context) { @Provides @Singleton - internal fun provideTestCellHolder(): TestCellMemoryHolder { - return TestCellMemoryHolder() + internal fun provideConfigMemoryHolder(): ConfigMemoryHolder { + return ConfigMemoryHolder() } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt index ecd9388b..6f4dc333 100755 --- a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt +++ b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt @@ -8,7 +8,7 @@ import dagger.Provides import io.constructor.BuildConfig import io.constructor.data.interceptor.TokenInterceptor import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -50,7 +50,7 @@ class NetworkModule(private val context: Context) { @Provides @Singleton - internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, testCellMemoryHolder: TestCellMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, testCellMemoryHolder) + internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, configMemoryHolder) @Provides @Singleton diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index ec83b019..c0a0795f 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -5,7 +5,7 @@ import io.constructor.BuildConfig import io.constructor.data.DataManager import io.constructor.data.interceptor.TokenInterceptor import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.Group import io.constructor.data.model.SuggestionViewModel import io.constructor.util.RxSchedulersOverrideRule @@ -31,7 +31,7 @@ class ConstructorIoTest { private val ctx = mockk() private val pref = mockk() - private val testCellMemoryHolder = mockk() + private val configMemoryHolder = mockk() private val data = mockk() private var constructorIo = ConstructorIo private val sampleMillis = "1520000000000" @@ -44,7 +44,7 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 - constructorIo.testInit(ctx, "dummyKey", data, pref, testCellMemoryHolder) + constructorIo.testInit(ctx, "dummyKey", data, pref, configMemoryHolder) } @After @@ -210,13 +210,14 @@ class ConstructorIoTest { val mockServer = MockWebServer() every { pref.token } returns "123" every { pref.id } returns "1" - every { testCellMemoryHolder.testCellParams = any() } just Runs - every { testCellMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") + every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) + every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") constructorIo.setTestCellValues("1" to "2", "3" to "4") - verify(exactly = 1) { testCellMemoryHolder.testCellParams = any() } + verify(exactly = 1) { configMemoryHolder.testCellParams = any() } mockServer.start() mockServer.enqueue(MockResponse()) - var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, testCellMemoryHolder)).build() + var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() var recordedRequest = mockServer.takeRequest() assert(recordedRequest.path.contains("ef-1=2")) diff --git a/library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt b/library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt similarity index 60% rename from library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt rename to library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt index c6c83b3a..8d9c675e 100644 --- a/library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt +++ b/library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt @@ -6,19 +6,19 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class TestCellMemoryHolderTest { +class ConfigMemoryHolderTest { - private lateinit var testCellMemoryHolder: TestCellMemoryHolder + private lateinit var configMemoryHolder: ConfigMemoryHolder @Before fun setUp() { - testCellMemoryHolder = TestCellMemoryHolder() + configMemoryHolder = ConfigMemoryHolder() } @Test fun verifyTestCellsWrittenAndEncoded() { - testCellMemoryHolder.testCellParams = listOf("1" to "2", "3" to "4") - val params = testCellMemoryHolder.testCellParams + configMemoryHolder.testCellParams = listOf("1" to "2", "3" to "4") + val params = configMemoryHolder.testCellParams assert(params[0]!!.first == "ef-1" && params[0]!!.second == "2") assert(params[1]!!.first == "ef-3" && params[1]!!.second == "4") } From 761e22cb4b2d8ed799ef456b509f2c5efef42b8f Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 25 Oct 2018 06:25:56 +0200 Subject: [PATCH 05/17] rename api_key (#22) --- .../java/io/constructor/core/Constants.kt | 2 +- .../data/interceptor/TokenInterceptor.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 28 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6a4ce316..714ee902 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -20,7 +20,7 @@ class Constants { const val ORIGINAL_QUERY = "original_query" const val CLIENT = "c" const val EVENT = "tr" - const val AUTOCOMPLETE_KEY = "autocomplete_key" + const val API_KEY = "key" const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index 584c02e1..fe9145f7 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -14,7 +14,7 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() val builder = request.url().newBuilder() - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, preferencesHelper.token) + .addQueryParameter(Constants.QueryConstants.API_KEY, preferencesHelper.token) .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index c0a0795f..45efdef0 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -53,7 +53,7 @@ class ConstructorIoTest { @Test fun verifySelectUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&autocomplete_key=testKey" + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" val searchQuery = "dog" val term = "hot dogs" val urlBuilder = HttpUrl.Builder().scheme("https") @@ -70,20 +70,20 @@ class ConstructorIoTest { .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_CLICK) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) } @Test fun verifyGetSuggestionsUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/dog?autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/autocomplete/dog?key=testKey&_dt=1520000000000" val searchQuery = "dog" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") .addPathSegment(searchQuery) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -91,14 +91,14 @@ class ConstructorIoTest { @Test fun verifySessionStartUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, "session_start") - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -106,7 +106,7 @@ class ConstructorIoTest { @Test fun verifySearchClickThroughEvent() { - val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=cioand-0.1.0&s=1&autocomplete_section=Products&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=cioand-0.1.0&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") @@ -115,7 +115,7 @@ class ConstructorIoTest { .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, "Products") - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -123,14 +123,14 @@ class ConstructorIoTest { @Test fun verifySearchLoadedEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_SEARCH_RESULTS) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -138,7 +138,7 @@ class ConstructorIoTest { @Test fun verifyInputFocusEvent() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&i=user_id&s=1&action=focus&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -146,7 +146,7 @@ class ConstructorIoTest { .addQueryParameter(Constants.QueryConstants.IDENTITY, "user_id") .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_INPUT_FOCUS) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -184,7 +184,7 @@ class ConstructorIoTest { @Test fun verifySearchUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&autocomplete_key=testKey" + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" val originalQuery = "dog" val term = "hot dogs" val urlBuilder = HttpUrl.Builder().scheme("https") @@ -200,7 +200,7 @@ class ConstructorIoTest { .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_SEARCH) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) } From a0712445362589ca897cb75bf8ed3b8770cea25c Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 6 Nov 2018 00:14:56 +0100 Subject: [PATCH 06/17] added config object (#21) * added config object * use autocomplete result limits only in autocomplete calls --- .../java/io/constructor/core/ConstructorIo.kt | 34 +++++++++---------- .../constructor/core/ConstructorIoConfig.kt | 8 +++++ .../java/io/constructor/data/DataManager.kt | 2 +- .../data/interceptor/TokenInterceptor.kt | 3 -- .../constructor/data/remote/ConstructorApi.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 5 ++- .../io/constructor/data/DataManagerTest.kt | 11 +++--- .../java/io/constructor/sample/SampleApp.kt | 5 +-- 8 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 library/src/main/java/io/constructor/core/ConstructorIoConfig.kt diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 605c8e41..44e98f21 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -2,10 +2,11 @@ package io.constructor.core import android.annotation.SuppressLint import android.content.Context -import io.constructor.BuildConfig +import io.constructor.data.ConstructorData import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.Suggestion import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent @@ -15,6 +16,7 @@ import io.constructor.util.broadcastIntent import io.constructor.util.d import io.constructor.util.e import io.constructor.util.urlEncode +import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.* @@ -49,8 +51,7 @@ object ConstructorIo { })) } - fun init(context: Context?, apiKey: String, autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, - Constants.QueryValues.PRODUCTS to 0), defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { + fun init(context: Context?, constructorIoConfig: ConstructorIoConfig) { if (context == null) { throw IllegalStateException("context is null, please init library using ConstructorIo.with(context)") } @@ -58,10 +59,11 @@ object ConstructorIo { dataManager = component.dataManager() preferenceHelper = component.preferenceHelper() configMemoryHolder = component.configMemoryHolder() - configMemoryHolder.autocompleteResultCount = autocompleteResultCount - preferenceHelper.token = apiKey + configMemoryHolder.autocompleteResultCount = constructorIoConfig.autocompleteResultCount + configMemoryHolder.testCellParams = constructorIoConfig.testCells + preferenceHelper.token = constructorIoConfig.apiKey - preferenceHelper.defaultItemSection = defaultItemSection + preferenceHelper.defaultItemSection = constructorIoConfig.defaultItemSection if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() } @@ -71,15 +73,7 @@ object ConstructorIo { fun getClientId() = preferenceHelper.id - fun setTestCellValues(pair1: Pair, pair2: Pair? = null, pair3: Pair? = null) { - configMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) - } - - fun clearTestCellValues() { - configMemoryHolder.testCellParams = emptyList() - } - - internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { + internal fun testInit(context: Context?, constructorIoConfig: ConstructorIoConfig, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { if (context == null) { throw IllegalStateException("Context is null, please init library using ConstructorIo.with(context)") } @@ -87,13 +81,19 @@ object ConstructorIo { this.dataManager = dataManager this.preferenceHelper = preferenceHelper this.configMemoryHolder = configMemoryHolder - preferenceHelper.token = apiKey + preferenceHelper.token = constructorIoConfig.apiKey if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() } } - fun getAutocompleteResults(query: String) = dataManager.getAutocompleteResults(query) + fun getAutocompleteResults(query: String): Observable?>> { + val params = mutableListOf>() + configMemoryHolder.autocompleteResultCount?.entries?.forEach { + params.add(Pair(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString())) + } + return dataManager.getAutocompleteResults(query, params.toTypedArray()) + } fun trackSelect(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) diff --git a/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt b/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt new file mode 100644 index 00000000..61356839 --- /dev/null +++ b/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt @@ -0,0 +1,8 @@ +package io.constructor.core + +import io.constructor.BuildConfig + +data class ConstructorIoConfig(val apiKey: String, + val autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0), + val defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION, + val testCells: List> = emptyList()) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 947f2bb0..69836bca 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton class DataManager @Inject constructor(private val constructorApi: ConstructorApi) { - fun getAutocompleteResults(text: String): Observable?>> = constructorApi.getSuggestions(text).map { + fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getSuggestions(text, params.toMap()).map { if (!it.isError) { it.response()?.let { if (it.isSuccessful) { diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index fe9145f7..fe2151f0 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -23,9 +23,6 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref builder.addQueryParameter(it.first, it.second) } } - configMemoryHolder.autocompleteResultCount?.entries?.forEach { - builder.addQueryParameter(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString()) - } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index b3ccb8a6..bf4df469 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -12,7 +12,7 @@ import retrofit2.http.QueryMap interface ConstructorApi { @GET(ApiPaths.URL_GET_SUGGESTIONS) - fun getSuggestions(@Path("value") value: String): Single> + fun getSuggestions(@Path("value") value: String, @QueryMap data: Map): Single> @GET(ApiPaths.URL_SELECT_EVENT) fun trackSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 45efdef0..949d884c 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -44,7 +44,8 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 - constructorIo.testInit(ctx, "dummyKey", data, pref, configMemoryHolder) + constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", + testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) } @After @@ -213,8 +214,6 @@ class ConstructorIoTest { every { configMemoryHolder.testCellParams = any() } just Runs every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") - constructorIo.setTestCellValues("1" to "2", "3" to "4") - verify(exactly = 1) { configMemoryHolder.testCellParams = any() } mockServer.start() mockServer.enqueue(MockResponse()) var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 530efc43..9cf58c93 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -27,7 +27,7 @@ class DataManagerTest { @Test fun getSuggestions() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -36,7 +36,7 @@ class DataManagerTest { @Test fun getSuggestionsBadServerResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.networkError @@ -45,7 +45,7 @@ class DataManagerTest { @Test fun getSuggestionsException() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.error(Exception())) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.error(Exception())) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isError @@ -54,7 +54,7 @@ class DataManagerTest { @Test fun getSuggestionsUnexpectedDataResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -63,7 +63,8 @@ class DataManagerTest { @Test fun getSuggestionsEmptyResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) + every { constructorApi.getSuggestions("titanic", any() + ) } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isEmpty diff --git a/sample/src/main/java/io/constructor/sample/SampleApp.kt b/sample/src/main/java/io/constructor/sample/SampleApp.kt index 724ff37b..5bfacc5c 100755 --- a/sample/src/main/java/io/constructor/sample/SampleApp.kt +++ b/sample/src/main/java/io/constructor/sample/SampleApp.kt @@ -2,12 +2,13 @@ package io.constructor.sample import android.app.Application import io.constructor.core.ConstructorIo +import io.constructor.core.ConstructorIoConfig class SampleApp : Application() { override fun onCreate() { super.onCreate() - ConstructorIo.init(this, "key_OucJxxrfiTVUQx0C") - ConstructorIo.setTestCellValues("ab" to "cd", "11" to "22") + ConstructorIo.init(this, ConstructorIoConfig("key_OucJxxrfiTVUQx0C", + testCells = listOf("ab" to "cd", "11" to "22"))) } } \ No newline at end of file From 73e0de059192203648523a8aadffc341b04e307e Mon Sep 17 00:00:00 2001 From: qbasso Date: Sat, 12 Jan 2019 18:58:30 +0100 Subject: [PATCH 07/17] ch3426 add uid parameter (#23) --- library/src/main/java/io/constructor/core/Constants.kt | 1 + library/src/main/java/io/constructor/core/ConstructorIo.kt | 6 ++++++ .../io/constructor/data/interceptor/TokenInterceptor.kt | 3 +++ .../java/io/constructor/data/memory/ConfigMemoryHolder.kt | 2 ++ .../src/test/java/io/constructor/core/ConstructorIoTest.kt | 2 ++ sample/src/main/java/io/constructor/sample/SampleApp.kt | 1 + 6 files changed, 15 insertions(+) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 714ee902..6b5d0949 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -24,6 +24,7 @@ class Constants { const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" + const val USER_ID = "ui" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 44e98f21..99ee9117 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -32,6 +32,12 @@ object ConstructorIo { private lateinit var context: Context private var disposable = CompositeDisposable() + var userId: String? + get() = configMemoryHolder.userId + set(value) { + configMemoryHolder.userId = value + } + internal val component: AppComponent by lazy { DaggerAppComponent.builder() .appModule(AppModule(context)) diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index fe2151f0..98e5cc09 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -23,6 +23,9 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref builder.addQueryParameter(it.first, it.second) } } + configMemoryHolder.userId?.let { + builder.addQueryParameter(Constants.QueryConstants.USER_ID, it) + } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) diff --git a/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt index 71f516d1..67b89d86 100644 --- a/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt +++ b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt @@ -33,4 +33,6 @@ class ConfigMemoryHolder @Inject constructor() { } var autocompleteResultCount: Map? = null + + var userId: String? = null } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 949d884c..b7656db5 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -212,6 +212,7 @@ class ConstructorIoTest { every { pref.token } returns "123" every { pref.id } returns "1" every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.userId } returns "uid" every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") mockServer.start() @@ -220,6 +221,7 @@ class ConstructorIoTest { client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() var recordedRequest = mockServer.takeRequest() assert(recordedRequest.path.contains("ef-1=2")) + assert(recordedRequest.path.contains("ui=uid")) } @Test diff --git a/sample/src/main/java/io/constructor/sample/SampleApp.kt b/sample/src/main/java/io/constructor/sample/SampleApp.kt index 5bfacc5c..03a9ff29 100755 --- a/sample/src/main/java/io/constructor/sample/SampleApp.kt +++ b/sample/src/main/java/io/constructor/sample/SampleApp.kt @@ -10,5 +10,6 @@ class SampleApp : Application() { super.onCreate() ConstructorIo.init(this, ConstructorIoConfig("key_OucJxxrfiTVUQx0C", testCells = listOf("ab" to "cd", "11" to "22"))) + ConstructorIo.userId = "uid" } } \ No newline at end of file From 541a0d5a976f933f77679e1744936f37a7a3c9ef Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 31 Jan 2019 06:55:44 +0100 Subject: [PATCH 08/17] ch4501 added purchase track event (#25) --- .../main/java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 14 ++++++++++++++ .../main/java/io/constructor/data/DataManager.kt | 8 ++++++-- .../java/io/constructor/data/remote/ApiPaths.kt | 1 + .../io/constructor/data/remote/ConstructorApi.kt | 3 +++ .../io/constructor/core/ConstructorIoTest.kt | 16 ++++++++++++---- .../java/io/constructor/data/DataManagerTest.kt | 7 +++++++ 7 files changed, 44 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6b5d0949..6acba189 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -22,6 +22,7 @@ class Constants { const val EVENT = "tr" const val API_KEY = "key" const val NUM_RESULTS = "num_results_" + const val CUSTOMER_ID = "customer_ids" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" const val USER_ID = "ui" diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 99ee9117..34e7bdce 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -187,4 +187,18 @@ object ConstructorIo { })) } + fun trackPurchase(clientIds: Array, sectionName: String? = null, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + val sectionNameParam = sectionName ?: preferenceHelper.defaultItemSection + val params = mutableListOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) + clientIds.forEach { params.add(Constants.QueryConstants.CUSTOMER_ID to it) } + disposable.add(dataManager.trackPurchase(params.toTypedArray()).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Input focus event error: ${t.message}") + })) + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 69836bca..ffb575a1 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -45,12 +45,16 @@ constructor(private val constructorApi: ConstructorApi) { return constructorApi.trackSearchResultClickThrough(term, itemId, position, params.toMap()) } - fun trackSearchResultLoaded(term: String, reultCount: Int, params: Array>): Completable { - return constructorApi.trackSearchResultLoaded(term, reultCount, params.toMap()) + fun trackSearchResultLoaded(term: String, resultCount: Int, params: Array>): Completable { + return constructorApi.trackSearchResultLoaded(term, resultCount, params.toMap()) } fun trackInputFocus(term: String?, params: Array>): Completable { return constructorApi.trackInputFocus(term, params.toMap()) } + fun trackPurchase(params: Array>): Completable { + return constructorApi.trackPurchase(params.toMap()) + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index 500fe72a..f4cf1baa 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -8,5 +8,6 @@ object ApiPaths { const val URL_CONVERT_EVENT = "autocomplete/{term}/conversion" const val URL_CLICK_THROUGH_EVENT = "autocomplete/{term}/click_through" const val URL_BEHAVIOR = "behavior" + const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index bf4df469..348e9ffc 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -34,4 +34,7 @@ interface ConstructorApi { @GET(ApiPaths.URL_BEHAVIOR) fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable + + @GET(ApiPaths.URL_PURCHASE) + fun trackPurchase(@QueryMap params: Map): Completable } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index b7656db5..cc7dcedf 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -92,7 +92,7 @@ class ConstructorIoTest { @Test fun verifySessionStartUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=session_start&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -107,7 +107,7 @@ class ConstructorIoTest { @Test fun verifySearchClickThroughEvent() { - val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=cioand-0.1.0&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=${BuildConfig.CLIENT_VERSION}&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") @@ -124,7 +124,7 @@ class ConstructorIoTest { @Test fun verifySearchLoadedEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=search-results&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -139,7 +139,7 @@ class ConstructorIoTest { @Test fun verifyInputFocusEvent() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -269,4 +269,12 @@ class ConstructorIoTest { verify(exactly = 1) { data.trackInputFocus(any(), any()) } } + @Test + fun trackPurchase() { + every { pref.defaultItemSection } returns "Products" + every { data.trackPurchase(any()) } returns Completable.complete() + constructorIo.trackPurchase(arrayOf("id1")) + verify(exactly = 1) { data.trackPurchase(any()) } + } + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 9cf58c93..e787ca27 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -184,4 +184,11 @@ class DataManagerTest { verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } } + @Test + fun trackPurchase() { + every { constructorApi.trackPurchase(any()) } returns Completable.complete() + dataManager.trackPurchase(arrayOf()) + verify(exactly = 1) { constructorApi.trackPurchase(any()) } + } + } \ No newline at end of file From 8a56a9aea593bdf23b1d4c92e3b2fbe57f7f2c1a Mon Sep 17 00:00:00 2001 From: qbasso Date: Wed, 6 Feb 2019 08:41:42 +0100 Subject: [PATCH 09/17] ch3226 match tracking calls singatures to iOS calls (#26) * ch3226 match tracking calls signatures to iOS calls * Fixed naming conventions * Updated README.md --- README.md | 38 ++++++++-- .../java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 47 ++++++------ .../java/io/constructor/data/DataManager.kt | 20 ++--- .../io/constructor/data/remote/ApiPaths.kt | 8 +- .../constructor/data/remote/ConstructorApi.kt | 18 ++--- .../io/constructor/service/OnSearchService.kt | 8 +- .../io/constructor/service/OnSelectService.kt | 8 +- .../ui/suggestion/SuggestionsPresenter.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 44 +++++------ .../io/constructor/data/DataManagerTest.kt | 76 +++++++++---------- .../io/constructor/sample/MainActivity.kt | 6 +- sample/src/main/res/layout/activity_main.xml | 4 +- 13 files changed, 153 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index c2c0ded4..f5386118 100755 --- a/README.md +++ b/README.md @@ -136,18 +136,42 @@ The Android Client sends behavioral events to [Constructor.io](http://constructo Three types of these events exist: 1. **General Events** are sent as needed when an instance of the Client is created or initialized -1. **Autocomplete Events** measure user interaction with autocomplete results and the `CIOAutocompleteViewController` sends them automatically. +1. **Autocomplete Events** measure user interaction with autocomplete results and extending from `BaseSuggestionFragment` sends them automatically. 1. **Search Events** measure user interaction with search results and the consuming app has to explicitly instrument them itself +### Autocomplete Events + +If you decide to extend from the `BaseSuggestionFragment`, these events are sent automatically. + ```kotlin import io.constructor.core.ConstructorIo -// Track search results loaded (term, resultCount) -ConstructorIo.trackSearchResultLoaded("a search term", 123) +// Track when the user focuses into the search bar (searchTerm) +ConstructorIo.trackInputFocus("") -// Track search result click (term, itemId, position) -ConstructorIo.trackSearchResultClickThrough("a search term", "an item id", "1") +// Track when the user selects an autocomplete suggestion (searchTerm, originalQuery, sectionName) +ConstructorIo.trackAutocompleteSelect("toothpicks", "tooth", "Search Suggestions") -// Track conversion (item id, term, revenue) -constructorIO.trackConversion("an item id", "a search term", "45.00") +// Track when the user submits a search (searchTerm, originalQuery) +ConstructorIo.trackSearchSubmit("toothpicks", "tooth") ``` + +### Search Events + +These events should be sent manually by the consuming app. + +```kotlin +import io.constructor.core.ConstructorIo + +// Track when search results are loaded into view (searchTerm, resultCount) +ConstructorIo.trackSearchResultsLoaded("tooth", 789) + +// Track when a search result is clicked (itemName, customerId, searchTerm) +ConstructorIo.trackSearchResultClick("Fashionable Toothpicks", "1234567-AB", "tooth") + +// Track when a search result converts (itemName, customerId, revenue, searchTerm) +ConstructorIo.trackConversion("Fashionable Toothpicks", "1234567-AB", 12.99, "tooth") + +// Track when products are purchased (customerIds) +ConstructorIo.trackPurchase(customerIDs: ["123-AB", "456-CD"]) +``` \ No newline at end of file diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6acba189..a0e9167b 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -26,6 +26,7 @@ class Constants { const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" const val USER_ID = "ui" + const val TERM_UNKNOWN = "TERM_UNKNOWN" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 34e7bdce..d0b87aa6 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -6,8 +6,8 @@ import io.constructor.data.ConstructorData import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.Group import io.constructor.data.model.Suggestion -import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -101,19 +101,19 @@ object ConstructorIo { return dataManager.getAutocompleteResults(query, params.toTypedArray()) } - fun trackSelect(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() - suggestion.group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } - suggestion.group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackSelect(suggestion.term, + group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } + group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } + disposable.add(dataManager.trackAutocompleteSelect(searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to suggestion.section!!, - Constants.QueryConstants.ORIGINAL_QUERY to query, + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK), encodedParams.toTypedArray()) .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to query) + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) }, { t -> t.printStackTrace() errorCallback?.invoke(t) @@ -121,17 +121,17 @@ object ConstructorIo { })) } - fun trackSearch(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { + fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() - suggestion.group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } - suggestion.group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackSearch(suggestion.term, + group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } + group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } + disposable.add(dataManager.trackSearchSubmit(searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.ORIGINAL_QUERY to query, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH), encodedParams.toTypedArray()) .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to query) + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) }, { it.printStackTrace() errorCallback?.invoke(it) @@ -139,11 +139,11 @@ object ConstructorIo { })) } - fun trackConversion(itemId: String, term: String = "TERM_UNKNOWN", revenue: String? = null, errorCallback: ConstructorError = null) { + fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackConversion(term, itemId, revenue, + disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, "%.2f".format(revenue), arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to preferenceHelper.defaultItemSection)).subscribeOn(Schedulers.io()) + Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection))).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) @@ -151,21 +151,22 @@ object ConstructorIo { })) } - fun trackSearchResultClickThrough(term: String, itemId: String, position: String? = null, errorCallback: ConstructorError = null) { + fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackSearchResultClickThrough(term, itemId, position, + val sName = sectionName ?: preferenceHelper.defaultItemSection + disposable.add(dataManager.trackSearchResultClick(itemName, customerId, searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to preferenceHelper.defaultItemSection)).subscribeOn(Schedulers.io()) + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName)).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Conversion click through event error: ${t.message}") + e("Search result click event error: ${t.message}") })) } - fun trackSearchResultLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { + fun trackSearchResultsLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackSearchResultLoaded(term, resultCount, + disposable.add(dataManager.trackSearchResultsLoaded(term, resultCount, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).subscribeOn(Schedulers.io()) .subscribe({}, { t -> diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index ffb575a1..d4582b01 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -25,28 +25,28 @@ constructor(private val constructorApi: ConstructorApi) { } }.toObservable() - fun trackSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackSelect(term, params.toMap(), encodedParams.toMap()) + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } - fun trackSearch(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackSearch(term, params.toMap(), encodedParams.toMap()) + fun trackSearchSubmit(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackSearchSubmit(term, params.toMap(), encodedParams.toMap()) } fun trackSessionStart(params: Array>): Completable { return constructorApi.trackSessionStart(params.toMap()) } - fun trackConversion(term: String, itemId: String, revenue: String? = null, params: Array> = arrayOf()): Completable { - return constructorApi.trackConversion(term, itemId, revenue, params.toMap()) + fun trackConversion(term: String, itemName: String, customerId: String, revenue: String? = null, params: Array> = arrayOf()): Completable { + return constructorApi.trackConversion(term, itemName, customerId, revenue, params.toMap()) } - fun trackSearchResultClickThrough(term: String, itemId: String, position: String? = null, params: Array> = arrayOf()): Completable { - return constructorApi.trackSearchResultClickThrough(term, itemId, position, params.toMap()) + fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf()): Completable { + return constructorApi.trackSearchResultTerm(term, itemName, customerId, params.toMap()) } - fun trackSearchResultLoaded(term: String, resultCount: Int, params: Array>): Completable { - return constructorApi.trackSearchResultLoaded(term, resultCount, params.toMap()) + fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array>): Completable { + return constructorApi.trackSearchResultsLoaded(term, resultCount, params.toMap()) } fun trackInputFocus(term: String?, params: Array>): Completable { diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index f4cf1baa..2264b226 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -2,11 +2,11 @@ package io.constructor.data.remote object ApiPaths { const val URL_GET_SUGGESTIONS = "autocomplete/{value}" - const val URL_SELECT_EVENT = "autocomplete/{term}/select" - const val URL_SEARCH_EVENT = "autocomplete/{term}/search" + const val URL_AUTOCOMPLETE_SELECT_EVENT = "autocomplete/{term}/select" + const val URL_SEARCH_SUBMIT_EVENT = "autocomplete/{term}/search" const val URL_SESSION_START_EVENT = "behavior" - const val URL_CONVERT_EVENT = "autocomplete/{term}/conversion" - const val URL_CLICK_THROUGH_EVENT = "autocomplete/{term}/click_through" + const val URL_CONVERSION_EVENT = "autocomplete/{term}/conversion" + const val URL_SEARCH_RESULT_CLICK_EVENT = "autocomplete/{term}/click_through" const val URL_BEHAVIOR = "behavior" const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 348e9ffc..0a04d8ca 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -14,23 +14,23 @@ interface ConstructorApi { @GET(ApiPaths.URL_GET_SUGGESTIONS) fun getSuggestions(@Path("value") value: String, @QueryMap data: Map): Single> - @GET(ApiPaths.URL_SELECT_EVENT) - fun trackSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable + @GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT) + fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable - @GET(ApiPaths.URL_SEARCH_EVENT) - fun trackSearch(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable + @GET(ApiPaths.URL_SEARCH_SUBMIT_EVENT) + fun trackSearchSubmit(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_SESSION_START_EVENT) fun trackSessionStart(@QueryMap params: Map): Completable - @GET(ApiPaths.URL_CONVERT_EVENT) - fun trackConversion(@Path("term") term: String, @Query("item_id") itemId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable + @GET(ApiPaths.URL_CONVERSION_EVENT) + fun trackConversion(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable - @GET(ApiPaths.URL_CLICK_THROUGH_EVENT) - fun trackSearchResultClickThrough(@Path("term") term: String, @Query("item_id") itemId: String, @Query("position") position: String?, @QueryMap params: Map): Completable + @GET(ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT) + fun trackSearchResultTerm(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) - fun trackSearchResultLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable + fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable diff --git a/library/src/main/java/io/constructor/service/OnSearchService.kt b/library/src/main/java/io/constructor/service/OnSearchService.kt index 362657b7..23d6613e 100755 --- a/library/src/main/java/io/constructor/service/OnSearchService.kt +++ b/library/src/main/java/io/constructor/service/OnSearchService.kt @@ -18,11 +18,11 @@ class OnSearchService : IntentService("OnSearchService") { } } - override fun onHandleIntent(intent: Intent?) { - val query: String? = intent?.getStringExtra(Constants.EXTRA_QUERY) - val suggestion: SuggestionViewModel = intent?.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel + override fun onHandleIntent(intent: Intent) { + val query: String = intent.getStringExtra(Constants.EXTRA_QUERY) + val suggestion: SuggestionViewModel = intent.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel if (!suggestion.term.isBlank()) { - ConstructorIo.trackSearch(query!!, suggestion) + ConstructorIo.trackSearchSubmit(suggestion.term, query, suggestion.group) } } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/service/OnSelectService.kt b/library/src/main/java/io/constructor/service/OnSelectService.kt index 52715ec8..ab97e34e 100755 --- a/library/src/main/java/io/constructor/service/OnSelectService.kt +++ b/library/src/main/java/io/constructor/service/OnSelectService.kt @@ -22,11 +22,11 @@ class OnSelectService : IntentService("OnSelectService") { } - override fun onHandleIntent(intent: Intent?) { - val query: String? = intent?.getStringExtra(Constants.EXTRA_QUERY) - val suggestion: SuggestionViewModel = intent?.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel + override fun onHandleIntent(intent: Intent) { + val query: String = intent.getStringExtra(Constants.EXTRA_QUERY) + val suggestion: SuggestionViewModel = intent.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel if (!suggestion.term.isBlank()) { - ConstructorIo.trackSelect(query!!, suggestion) + ConstructorIo.trackAutocompleteSelect(suggestion.term, query, suggestion.section!!, suggestion.group) } } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index 05c7ed0d..0bdfecd5 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -48,7 +48,7 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter?>>()).subscribe { data -> data.onValue { - ConstructorIo.trackSearchResultLoaded(text, it!!.size) + ConstructorIo.trackSearchResultsLoaded(text, it!!.size) mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) } data.onError { diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index cc7dcedf..f88c845d 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -91,7 +91,7 @@ class ConstructorIoTest { } @Test - fun verifySessionStartUrl() { + fun verifySessionStartEventUrl() { val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=session_start&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") @@ -106,7 +106,7 @@ class ConstructorIoTest { } @Test - fun verifySearchClickThroughEvent() { + fun verifySearchResultClickEventUrl() { val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=${BuildConfig.CLIENT_VERSION}&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") @@ -123,7 +123,7 @@ class ConstructorIoTest { } @Test - fun verifySearchLoadedEventUrl() { + fun verifySearchResultsLoadedEventUrl() { val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=search-results&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") @@ -154,31 +154,31 @@ class ConstructorIoTest { } @Test - fun trackSelectSuccess() { + fun trackAutocompleteSelectSuccess() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSelect(any(), any(), any()) } returns Completable.complete() - constructorIo.trackSelect("doggy dog", dummySuggestion) + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.complete() + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } } } @Test - fun trackSelectError() { + fun trackAutocompleteSelectError() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSelect(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackSelect("doggy dog", dummySuggestion) + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } } } @Test - fun trackSearchSuccess() { + fun trackSearchSubmitSuccess() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearch(any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearch("doggy dog", dummySuggestion) + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } } } @@ -225,11 +225,11 @@ class ConstructorIoTest { } @Test - fun trackSearchError() { + fun trackSearchSubmitError() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearch(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackSearch("doggy dog", dummySuggestion) + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } } } @@ -237,17 +237,17 @@ class ConstructorIoTest { @Test fun trackConversion() { every { pref.defaultItemSection } returns "Products" - every { data.trackConversion(any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackConversion(itemId = "1") - verify(exactly = 1) { data.trackConversion(any(), any(), any(), any()) } + every { data.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackConversion("corn", "id1", 11.99) + verify(exactly = 1) { data.trackConversion("TERM_UNKNOWN", any(), any(), any(), any()) } } @Test - fun trackSearchResultClickThrough() { + fun trackSearchResultClick() { every { pref.defaultItemSection } returns "Products" - every { data.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearchResultClickThrough("1", "1") - verify(exactly = 1) { data.trackSearchResultClickThrough(any(), any(), any(), any()) } + every { data.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchResultClick("1", "1") + verify(exactly = 1) { data.trackSearchResultClick(any(), any(), any(), any()) } } @Test diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index e787ca27..2ebfad37 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -72,37 +72,37 @@ class DataManagerTest { } @Test - fun trackSelect() { - every { constructorApi.trackSelect(any(), any(),any()) } returns Completable.complete() - dataManager.trackSelect("titanic") - verify(exactly = 1) { constructorApi.trackSelect(any(), any(), any())} + fun trackAutocompleteSelect() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.complete() + dataManager.trackAutocompleteSelect("titanic") + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} } @Test - fun trackSelectError() { - every { constructorApi.trackSelect(any(), any(),any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSelect("titanic").test() + fun trackAutocompleteSelectError() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.error(Exception()) + val observer = dataManager.trackAutocompleteSelect("titanic").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSelect(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} } @Test - fun trackSearch() { - every { constructorApi.trackSearch(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearch("titanic") - verify(exactly = 1) { constructorApi.trackSearch(any(), any(), any())} + fun trackSearchSubmit() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchSubmit("titanic") + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} } @Test - fun trackSearchError() { - every { constructorApi.trackSearch(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearch("titanic").test() + fun trackSearchSubmitError() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchSubmit("titanic").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearch(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} } @Test @@ -124,49 +124,49 @@ class DataManagerTest { @Test fun trackConversion() { - every { constructorApi.trackConversion(any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackConversion("testTerm", "1") - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any())} + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackConversion("testTerm", "item1", "id1", "11.99") + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} } @Test fun trackConversionError() { - every { constructorApi.trackConversion(any(), any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackConversion("testTerm", "1").test() + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackConversion("testTerm", "item1", "id1").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} } @Test - fun trackSearchResultClickThrough() { - every { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultClickThrough("term", "1") - verify(exactly = 1) { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any())} + fun trackSearchResultClick() { + every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultClick("term", "id1", "term1") + verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} } @Test - fun trackSearchResultClickThroughError() { - every { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchResultClickThrough("term", "1").test() + fun trackSearchResultClickError() { + every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchResultClick("term", "1", "term1").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} } @Test - fun trackSearchResultLoaded() { - every { constructorApi.trackSearchResultLoaded(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultLoaded("term", 10, arrayOf()) - verify(exactly = 1) { constructorApi.trackSearchResultLoaded(any(), any(), any())} + fun trackSearchResultsLoaded() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultsLoaded("term", 10, arrayOf()) + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} } @Test - fun trackSearchResultLoadedError() { - every { constructorApi.trackSearchResultLoaded(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchResultLoaded("term", 10, arrayOf()).test() + fun trackSearchResultsLoadedError() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchResultsLoaded("term", 10, arrayOf()).test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultLoaded(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} } @Test diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 6186b2d4..61a7a5a5 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -14,8 +14,8 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) button.setOnClickListener { startActivity(Intent(this, SampleActivity::class.java)) } button2.setOnClickListener { startActivity(Intent(this, SampleActivityCustom::class.java)) } - button3.setOnClickListener { ConstructorIo.trackConversion("testId", revenue = "$11.99") } - button4.setOnClickListener { ConstructorIo.trackSearchResultClickThrough("testTerm", "testId", "1") } - button5.setOnClickListener { ConstructorIo.trackSearchResultLoaded("testTerm", Random().nextInt(99) + 1) } + button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.99) } + button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } + button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 312e6efa..ea623bb5 100755 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -41,7 +41,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="Trigger SearchThrough" + android:text="Trigger Search Result Click" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button3" /> @@ -51,7 +51,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="Trigger SearchResult Loaded" + android:text="Trigger Search Result Loaded" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button4" /> From 5689e525469239470c21447739ac1d1313a30465 Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 21 Feb 2019 06:26:34 +0100 Subject: [PATCH 10/17] ch3225 foreground method added (#27) * ch3225 foreground method added * Removed force session internal in foreground call (but kept signature) --- .../main/java/io/constructor/core/ConstructorIo.kt | 4 ++++ .../io/constructor/data/local/PreferencesHelper.kt | 4 ++-- .../ui/suggestion/SuggestionsPresenter.kt | 13 ++++++------- .../src/main/java/io/constructor/util/Extensions.kt | 9 +++++++++ .../constructor/util/rx/scheduler/SchedulerUtils.kt | 12 ------------ .../java/io/constructor/core/ConstructorIoTest.kt | 1 + .../main/java/io/constructor/sample/MainActivity.kt | 3 ++- 7 files changed, 24 insertions(+), 22 deletions(-) delete mode 100755 library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index d0b87aa6..4286a04c 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -93,6 +93,10 @@ object ConstructorIo { } } + fun appMovedToForeground() { + preferenceHelper.getSessionId(sessionIncrementEventHandler) + } + fun getAutocompleteResults(query: String): Observable?>> { val params = mutableListOf>() configMemoryHolder.autocompleteResultCount?.entries?.forEach { diff --git a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt index 5c5aed59..c1ca2f5b 100755 --- a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt +++ b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt @@ -30,13 +30,13 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI get() = preferences.getLong(SESSION_LAST_ACCESS, System.currentTimeMillis()) set(value) = preferences.edit().putLong(SESSION_LAST_ACCESS, value).apply() - fun getSessionId(sessionIncrementAction: ((String) -> Unit)? = null): Int { + fun getSessionId(sessionIncrementAction: ((String) -> Unit)? = null, forceIncrement: Boolean = false): Int { if (!preferences.contains(SESSION_ID)) { return resetSession(sessionIncrementAction) } val sessionTime = lastSessionAccess val timeDiff = System.currentTimeMillis() - sessionTime - if (timeDiff > SESSION_TIME_THRESHOLD) { + if (timeDiff > SESSION_TIME_THRESHOLD || forceIncrement) { var sessionId = preferences.getInt(SESSION_ID, 1) preferences.edit().putInt(SESSION_ID, ++sessionId).apply() sessionIncrementAction?.invoke(sessionId.toString()) diff --git a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index 0bdfecd5..a042b03f 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -1,13 +1,11 @@ package io.constructor.ui.suggestion import io.constructor.core.ConstructorIo -import io.constructor.data.ConstructorData import io.constructor.data.local.PreferencesHelper -import io.constructor.data.model.Suggestion import io.constructor.features.base.BasePresenter import io.constructor.injection.ConfigPersistent import io.constructor.util.d -import io.constructor.util.rx.scheduler.SchedulerUtils +import io.constructor.util.io2ui import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit @@ -21,7 +19,7 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter()).subscribe({ query -> + disposables.add(mvpView.queryChanged().debounce(300, TimeUnit.MILLISECONDS).io2ui().subscribe({ query -> getSuggestions(query) }, {error -> error.printStackTrace() @@ -46,10 +44,11 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter?>>()).subscribe { data -> + disposables.add(ConstructorIo.getAutocompleteResults(text).io2ui().subscribe { data -> data.onValue { - ConstructorIo.trackSearchResultsLoaded(text, it!!.size) - mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) + it?.let { + mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) + } } data.onError { if (it is NoSuchElementException) { diff --git a/library/src/main/java/io/constructor/util/Extensions.kt b/library/src/main/java/io/constructor/util/Extensions.kt index 076aefa2..43f7aa26 100755 --- a/library/src/main/java/io/constructor/util/Extensions.kt +++ b/library/src/main/java/io/constructor/util/Extensions.kt @@ -6,6 +6,9 @@ import android.os.Parcelable import android.support.v4.content.LocalBroadcastManager import android.util.Base64 import android.util.Log +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import java.io.Serializable import java.net.URLEncoder @@ -43,3 +46,9 @@ fun String.base64Encode(): String? { fun String.base64Decode(): String { return String(Base64.decode(this, Base64.NO_WRAP or Base64.NO_PADDING)) } + +fun Observable.io2ui(): Observable { + return compose { + it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + } +} diff --git a/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt b/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt deleted file mode 100755 index 2ad83e8d..00000000 --- a/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.constructor.util.rx.scheduler - -/** - * Created by lam on 2/6/17. - */ - -object SchedulerUtils { - - fun ioToMain(): IoMainScheduler { - return IoMainScheduler() - } -} diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index f88c845d..1d8982b2 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -44,6 +44,7 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 + every { pref.getSessionId(any(), any()) } returns 1 constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) } diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 61a7a5a5..0a963b54 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -12,9 +12,10 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + ConstructorIo.appMovedToForeground() button.setOnClickListener { startActivity(Intent(this, SampleActivity::class.java)) } button2.setOnClickListener { startActivity(Intent(this, SampleActivityCustom::class.java)) } - button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.99) } + button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.0) } button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } } From b82374ed43dbbaf8bb5794e56c795bc7a5d8433b Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Wed, 20 Feb 2019 21:41:27 -0800 Subject: [PATCH 11/17] Version bump --- .gitignore | 4 +++- .project | 17 ----------------- .settings/org.eclipse.buildship.core.prefs | 13 ------------- README.md | 2 +- library/build.gradle | 2 +- 5 files changed, 5 insertions(+), 33 deletions(-) delete mode 100644 .project delete mode 100644 .settings/org.eclipse.buildship.core.prefs diff --git a/.gitignore b/.gitignore index 9262dfde..0dd8a9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ *iml *.iml */build -fastlane \ No newline at end of file +fastlane +.project +.settings/org.eclipse.buildship.core.prefs \ No newline at end of file diff --git a/.project b/.project deleted file mode 100644 index f3b37838..00000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - constructorio-client-android - Project constructorio-client-android created by Buildship. - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.buildship.core.gradleprojectnature - - diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 4ac9cbc0..00000000 --- a/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -arguments= -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.project.dir= -eclipse.preferences.version=1 -gradle.user.home=/usr/local/opt/gradle -java.home= -jvm.arguments= -offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true diff --git a/README.md b/README.md index 739a6647..40988974 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An Android Client for [Constructor.io](http://constructor.io/). [Constructor.io ## 1. Install -Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.1.0) to add the client to your project. +Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.2.0) to add the client to your project. ## 2. Retrieve an API key diff --git a/library/build.gradle b/library/build.gradle index f16d9de6..822fc324 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -20,7 +20,7 @@ android { targetSdkVersion 27 testInstrumentationRunner "${applicationId}.runner.RxAndroidJUnitRunner" versionCode 1 - versionName '1.1.0' + versionName '1.2.0' buildConfigField("String", "CLIENT_VERSION", "\"cioand-${versionName}\"") buildConfigField("String", "AUTOCOMPLETE_SECTION", "\"Products\"") buildConfigField("String", "BASE_API_URL", "\"https://ac.cnstrc.com\"") From b2981cd4ed06304ce5cc29b3e01ffba42d2c0e5f Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 26 Feb 2019 09:05:05 +0100 Subject: [PATCH 12/17] ch3305 add lint to build (#29) --- library/build.gradle | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/build.gradle b/library/build.gradle index 822fc324..1488008b 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -78,8 +78,18 @@ android { androidExtensions { experimental = true } + + libraryVariants.all { variant -> + variant.outputs.each { output -> + def lintTask = tasks["lint${variant.name.capitalize()}"] + output.assemble.dependsOn lintTask + } + } + } + + apply from: 'dependencies.gradle' dependencies { From 31a2aafc58ddad7d8064ae8d6ec6860bc0829f0f Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 5 Mar 2019 07:40:44 +0100 Subject: [PATCH 13/17] updated libraries/gradle & added code coverage reports (#33) --- build.gradle | 5 +- gradle/wrapper/gradle-wrapper.properties | 4 +- library/build.gradle | 73 +++++++++++++++++-- library/dependencies.gradle | 2 +- .../constructor/features/base/BaseActivity.kt | 11 +-- sample/build.gradle | 13 ++-- 6 files changed, 81 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index a222e170..57530186 100755 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,14 @@ buildscript { - ext.kotlin_version = '1.2.41' + ext.kotlin_version = '1.3.21' repositories { google() jcenter() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'org.jacoco:org.jacoco.core:0.8.3' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c471b180..fc3741ee 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon May 07 20:12:54 CEST 2018 +#Thu Feb 28 08:10:16 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/library/build.gradle b/library/build.gradle index 1488008b..eb0e59eb 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,12 +3,20 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'jacoco' group='com.github.Constructor-io' +jacoco { + toolVersion = '0.8.3' +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true +} + android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdkVersion 28 dexOptions { maxProcessCount 4 @@ -17,7 +25,7 @@ android { defaultConfig { minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 28 testInstrumentationRunner "${applicationId}.runner.RxAndroidJUnitRunner" versionCode 1 versionName '1.2.0' @@ -88,6 +96,61 @@ android { } +task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + + def buildDir = "build/" + + def coverageSourceDirs = [ + "src/main/java" + ] + + def excludedClasses = [ + '**/databinding/**/*.*', + '**/android/databinding/*Binding.*', + '**/BR.*', + '**/R.*', + '**/R$*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*_MembersInjector.*', + '**/Dagger*Component.*', + '**/Dagger*Component$Builder.*', + '**/*Module_*Factory.*', + '**/*Fragment*.*', + '**/*Activity*.*', + '**/*Adapter*.*', + '**/*ViewPager*.*', + '**/*ViewHolder*.*', + '**/*Module*.*' + ] + + def javaClasses = fileTree( + dir: "$buildDir/intermediates/classes/debug", + excludes: excludedClasses + ) + + def kotlinClasses = fileTree( + dir: "$buildDir/tmp/kotlin-classes/debug", + excludes: excludedClasses + ) + + classDirectories = files([ javaClasses ], [ kotlinClasses ]) + additionalSourceDirs = files(coverageSourceDirs) + sourceDirectories = files(coverageSourceDirs) + executionData = fileTree(dir: "$buildDir/jacoco", includes: [ + "testDebugUnitTest.exec" + ]) + + reports { + xml.enabled = true + html.enabled = true + } +} + +task getCoverage(type: Exec, dependsOn: 'jacocoUnitTestReport') { + group = "Reporting" + commandLine "open", "$buildDir/reports/jacoco/jacocoUnitTestReport/html/index.html" +} apply from: 'dependencies.gradle' @@ -104,8 +167,8 @@ dependencies { implementation supportLibs implementation networkLibs implementation otherLibs - implementation 'com.android.support:support-v4:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:cardview-v7:28.0.0' kapt annotationProcessorLibs kaptTest daggerCompiler kaptAndroidTest daggerCompiler diff --git a/library/dependencies.gradle b/library/dependencies.gradle index 41fd1ae9..fc99e893 100755 --- a/library/dependencies.gradle +++ b/library/dependencies.gradle @@ -1,6 +1,6 @@ ext { versions = [ - support : "27.1.1", + support : "28.0.0", moshi : "1.5.0", okHttp : "3.9.0", retrofit: '2.3.0', diff --git a/library/src/main/java/io/constructor/features/base/BaseActivity.kt b/library/src/main/java/io/constructor/features/base/BaseActivity.kt index 0677d23d..e98c0088 100755 --- a/library/src/main/java/io/constructor/features/base/BaseActivity.kt +++ b/library/src/main/java/io/constructor/features/base/BaseActivity.kt @@ -13,15 +13,6 @@ import io.constructor.injection.module.ActivityModule import io.constructor.util.d import java.util.concurrent.atomic.AtomicLong -/** - * Abstract activity that every other Activity in this application must implement. It provides the - * following functionality: - * - Handles creation of Dagger components and makes sure that instances of - * ConfigPersistentComponent are kept across configuration changes. - * - Set up and handles a GoogleApiClient instance that can be used to access the Google sign in - * api. - * - Handles signing out when an authentication error event is received. - */ abstract class BaseActivity : AppCompatActivity() { private var activityComponent: ActivityComponent? = null @@ -48,7 +39,7 @@ abstract class BaseActivity : AppCompatActivity() { componentsArray.put(activityId, configPersistentComponent) } else { d("Reusing ConfigPersistentComponent id=${activityId}") - configPersistentComponent = componentsArray.get(activityId) + configPersistentComponent = componentsArray.get(activityId)!! } activityComponent = configPersistentComponent.activityComponent(ActivityModule(this)) activityComponent?.inject(this) diff --git a/sample/build.gradle b/sample/build.gradle index ad4401e4..35ddde42 100755 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 27 + compileSdkVersion 28 defaultConfig { applicationId "io.constructor.sample" minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -22,7 +22,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - buildToolsVersion '27.0.3' } configurations.all { @@ -36,9 +35,9 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':library') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation "com.android.support:cardview-v7:27.1.1" - implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation "com.android.support:cardview-v7:28.0.0" + implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' } From b97b3873dcc3e88301588f0084d6bd0d17e65274 Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 5 Mar 2019 07:48:32 +0100 Subject: [PATCH 14/17] Ch2913/http tests (#30) * http tests * http tests --- library/build.gradle | 2 +- .../java/io/constructor/core/ConstructorIo.kt | 3 +- .../java/io/constructor/data/DataManager.kt | 4 +- .../constructor/data/remote/ConstructorApi.kt | 3 +- .../io/constructor/core/ConstructorIoTest.kt | 4 +- .../constructor/data/DataManagerHttpTest.kt | 439 ++++++++++++++++++ .../io/constructor/data/DataManagerTest.kt | 6 +- .../io/constructor/util/TestDataLoader.kt | 12 + 8 files changed, 462 insertions(+), 11 deletions(-) create mode 100755 library/src/test/java/io/constructor/data/DataManagerHttpTest.kt diff --git a/library/build.gradle b/library/build.gradle index eb0e59eb..428af783 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -161,7 +161,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' implementation 'io.reactivex.rxjava2:rxjava:2.1.8' implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' - testImplementation 'io.mockk:mockk:1.7.14' + testImplementation 'io.mockk:mockk:1.9.kotlin12' testImplementation 'org.robolectric:robolectric:3.6.1' testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0' implementation supportLibs diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 4286a04c..098979db 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -197,8 +197,7 @@ object ConstructorIo { val sectionNameParam = sectionName ?: preferenceHelper.defaultItemSection val params = mutableListOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) - clientIds.forEach { params.add(Constants.QueryConstants.CUSTOMER_ID to it) } - disposable.add(dataManager.trackPurchase(params.toTypedArray()).subscribeOn(Schedulers.io()) + disposable.add(dataManager.trackPurchase(clientIds.toList(), params.toTypedArray()).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index d4582b01..be1e76ce 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -53,8 +53,8 @@ constructor(private val constructorApi: ConstructorApi) { return constructorApi.trackInputFocus(term, params.toMap()) } - fun trackPurchase(params: Array>): Completable { - return constructorApi.trackPurchase(params.toMap()) + fun trackPurchase(customerIds: List, params: Array>): Completable { + return constructorApi.trackPurchase(customerIds, params.toMap()) } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 0a04d8ca..ab98edf0 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -1,5 +1,6 @@ package io.constructor.data.remote +import io.constructor.core.Constants import io.constructor.data.model.AutocompleteResult import io.reactivex.Completable import io.reactivex.Single @@ -36,5 +37,5 @@ interface ConstructorApi { fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable @GET(ApiPaths.URL_PURCHASE) - fun trackPurchase(@QueryMap params: Map): Completable + fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, @QueryMap params: Map): Completable } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 1d8982b2..897c4834 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -273,9 +273,9 @@ class ConstructorIoTest { @Test fun trackPurchase() { every { pref.defaultItemSection } returns "Products" - every { data.trackPurchase(any()) } returns Completable.complete() + every { data.trackPurchase(any(), any()) } returns Completable.complete() constructorIo.trackPurchase(arrayOf("id1")) - verify(exactly = 1) { data.trackPurchase(any()) } + verify(exactly = 1) { data.trackPurchase(any(), any()) } } } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt new file mode 100755 index 00000000..d3bb3471 --- /dev/null +++ b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt @@ -0,0 +1,439 @@ +package io.constructor.data + +import android.content.Context +import com.squareup.moshi.KotlinJsonAdapterFactory +import com.squareup.moshi.Moshi +import io.constructor.core.Constants +import io.constructor.data.interceptor.TokenInterceptor +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.remote.ApiPaths +import io.constructor.data.remote.ConstructorApi +import io.constructor.util.RxSchedulersOverrideRule +import io.constructor.util.TestDataLoader +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit + +class DataManagerHttpTest { + + @Rule + @JvmField val overrideSchedulersRule = RxSchedulersOverrideRule() + + private lateinit var constructorApi: ConstructorApi + + private val ctx = mockk() + private val pref = mockk() + private val configMemoryHolder = mockk() + + private lateinit var dataManager: DataManager + + private lateinit var mockServer: MockWebServer + + @Before + fun setup() { + every { pref.token } returns "123" + every { pref.id } returns "1" + every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.userId } returns "id1" + every { configMemoryHolder.testCellParams } returns emptyList() + mockServer = MockWebServer() + mockServer.start() + + val client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).readTimeout(4, TimeUnit.SECONDS).build() + + val moshi = Moshi + .Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + // Get an instance of Retrofit + val retrofit = Retrofit.Builder() + .baseUrl(mockServer.url("").toString()) + .client(client) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + + constructorApi = retrofit.create(ConstructorApi::class.java) + dataManager = DataManager(constructorApi) + } + + @Test + fun getAutocompleteResults() { + val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.get()!!.isNotEmpty() && it.get()!!.size == 5 + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsBadServerResponse() { + val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.networkError + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsTimeoutException() { + val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response.json")) + mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.isError + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsUnexpectedDataResponse() { + val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response_with_unexpected_data.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.get()!!.isNotEmpty() && it.get()!!.size == 5 + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun getAutocompleteResultsEmptyResponse() { + val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("empty_response.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getAutocompleteResults("titanic").test() + observer.assertComplete().assertValue { + it.isEmpty + } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteSelect() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteSelect500() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackAutocompleteTimeout() { + val path = "/" + ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackAutocompleteSelect("titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmit() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmit500() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSearchSubmitTimeout() { + val path = "/" + ApiPaths.URL_SEARCH_SUBMIT_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchSubmit("titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + } + + @Test + fun trackSessionStart() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackSessionStart500() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackSessionStartTimeout() { + val path = "/" + ApiPaths.URL_SESSION_START_EVENT + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSessionStart(arrayOf(Constants.QueryConstants.SESSION to "1")).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("s=1")) + } + + @Test + fun trackConversion() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackConversion500() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackConversionTimeout() { + val path = "/" + ApiPaths.URL_CONVERSION_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackConversion("titanic", "ship", "cid").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClick() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClick500() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultClickTimeout() { + val path = "/" + ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT.replace("{term}", "titanic") + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultClick("ship", "cid", "titanic").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("name=ship")) + assert(request.path.contains("customer_id=cid")) + } + + @Test + fun trackSearchResultLoaded() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackSearchResultLoaded500() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackSearchResultLoadedTimeout() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackSearchResultsLoaded("titanic", 10, arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_SEARCH_RESULTS}")) + } + + @Test + fun trackInputFocus() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackInputFocus500() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackInputFocusTimeout() { + val path = "/" + ApiPaths.URL_BEHAVIOR + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackInputFocus("titanic", arrayOf(Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_INPUT_FOCUS)).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.ACTION}=${Constants.QueryValues.EVENT_INPUT_FOCUS}")) + } + + @Test + fun trackPurchase() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + observer.assertComplete() + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + + @Test + fun trackPurchase500() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + observer.assertError { true } + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + + @Test + fun trackPurchaseTimeout() { + val path = "/" + ApiPaths.URL_PURCHASE + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + assert(request.path.startsWith(path)) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=1")) + assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) + } + +} \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 2ebfad37..697764b9 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -186,9 +186,9 @@ class DataManagerTest { @Test fun trackPurchase() { - every { constructorApi.trackPurchase(any()) } returns Completable.complete() - dataManager.trackPurchase(arrayOf()) - verify(exactly = 1) { constructorApi.trackPurchase(any()) } + every { constructorApi.trackPurchase(any(), any()) } returns Completable.complete() + dataManager.trackPurchase(listOf(), arrayOf()) + verify(exactly = 1) { constructorApi.trackPurchase(any(), any()) } } } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/util/TestDataLoader.kt b/library/src/test/java/io/constructor/util/TestDataLoader.kt index 2794d7ce..140a0693 100755 --- a/library/src/test/java/io/constructor/util/TestDataLoader.kt +++ b/library/src/test/java/io/constructor/util/TestDataLoader.kt @@ -7,6 +7,7 @@ import okio.Buffer import java.io.File import java.io.FileInputStream import java.io.IOException +import java.nio.charset.Charset object TestDataLoader { @@ -30,4 +31,15 @@ object TestDataLoader { return result } + fun loadAsString(fileName: String): String { + var result = "" + try { + result = File(TestDataLoader::class.java.classLoader.getResource(fileName).path).inputStream().readBytes().toString(Charset.defaultCharset()) + } catch (e: IOException) { + e.printStackTrace() + } + return result + } + + } From 0d326e12c9d1c2c9cc0a1647ef2f569aab10da07 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Mon, 4 Mar 2019 23:02:25 -0800 Subject: [PATCH 15/17] added revenue param to purchase tracking call (#34) --- .../src/main/java/io/constructor/core/ConstructorIo.kt | 8 +++++--- library/src/main/java/io/constructor/data/DataManager.kt | 4 ++-- .../java/io/constructor/data/remote/ConstructorApi.kt | 4 +++- .../test/java/io/constructor/core/ConstructorIoTest.kt | 6 +++--- .../test/java/io/constructor/data/DataManagerHttpTest.kt | 6 +++--- .../src/test/java/io/constructor/data/DataManagerTest.kt | 6 +++--- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 098979db..df83ba94 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -145,7 +145,8 @@ object ConstructorIo { fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, "%.2f".format(revenue), + val revenueString = revenue?.let { "%.2f".format(revenue) } + disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, revenueString, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection))).subscribeOn(Schedulers.io()) .subscribe({}, { t -> @@ -192,12 +193,13 @@ object ConstructorIo { })) } - fun trackPurchase(clientIds: Array, sectionName: String? = null, errorCallback: ConstructorError = null) { + fun trackPurchase(clientIds: Array, revenue: Double?, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val sectionNameParam = sectionName ?: preferenceHelper.defaultItemSection + val revenueString = revenue?.let { "%.2f".format(revenue) } val params = mutableListOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) - disposable.add(dataManager.trackPurchase(clientIds.toList(), params.toTypedArray()).subscribeOn(Schedulers.io()) + disposable.add(dataManager.trackPurchase(clientIds.toList(), revenueString, params.toTypedArray()).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index be1e76ce..80c44395 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -53,8 +53,8 @@ constructor(private val constructorApi: ConstructorApi) { return constructorApi.trackInputFocus(term, params.toMap()) } - fun trackPurchase(customerIds: List, params: Array>): Completable { - return constructorApi.trackPurchase(customerIds, params.toMap()) + fun trackPurchase(customerIds: List, revenue: String? = null, params: Array>): Completable { + return constructorApi.trackPurchase(customerIds, revenue, params.toMap()) } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index ab98edf0..128c4381 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -37,5 +37,7 @@ interface ConstructorApi { fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable @GET(ApiPaths.URL_PURCHASE) - fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, @QueryMap params: Map): Completable + fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, + @Query("revenue") revenue: String?, + @QueryMap params: Map): Completable } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 897c4834..66277685 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -273,9 +273,9 @@ class ConstructorIoTest { @Test fun trackPurchase() { every { pref.defaultItemSection } returns "Products" - every { data.trackPurchase(any(), any()) } returns Completable.complete() - constructorIo.trackPurchase(arrayOf("id1")) - verify(exactly = 1) { data.trackPurchase(any(), any()) } + every { data.trackPurchase(any(), any(), any()) } returns Completable.complete() + constructorIo.trackPurchase(arrayOf("id1"), 12.99) + verify(exactly = 1) { data.trackPurchase(any(), any(), any()) } } } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt index d3bb3471..1c2ad871 100755 --- a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt @@ -401,7 +401,7 @@ class DataManagerHttpTest { val path = "/" + ApiPaths.URL_PURCHASE val mockResponse = MockResponse().setResponseCode(204) mockServer.enqueue(mockResponse) - val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() observer.assertComplete() val request = mockServer.takeRequest() assert(request.path.startsWith(path)) @@ -414,7 +414,7 @@ class DataManagerHttpTest { val path = "/" + ApiPaths.URL_PURCHASE val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") mockServer.enqueue(mockResponse) - val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() observer.assertError { true } val request = mockServer.takeRequest() assert(request.path.startsWith(path)) @@ -428,7 +428,7 @@ class DataManagerHttpTest { val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) mockServer.enqueue(mockResponse) - val observer = dataManager.trackPurchase(listOf("1", "2"), arrayOf()).test() + val observer = dataManager.trackPurchase(listOf("1", "2"), "12.99", arrayOf()).test() observer.assertError(SocketTimeoutException::class.java) val request = mockServer.takeRequest() assert(request.path.startsWith(path)) diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 697764b9..b834292b 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -186,9 +186,9 @@ class DataManagerTest { @Test fun trackPurchase() { - every { constructorApi.trackPurchase(any(), any()) } returns Completable.complete() - dataManager.trackPurchase(listOf(), arrayOf()) - verify(exactly = 1) { constructorApi.trackPurchase(any(), any()) } + every { constructorApi.trackPurchase(any(), any(), any()) } returns Completable.complete() + dataManager.trackPurchase(listOf(), "12.99", arrayOf()) + verify(exactly = 1) { constructorApi.trackPurchase(any(), any(), any()) } } } \ No newline at end of file From 1c8b61d1fe05281fda166acedd124a70c591c43e Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 16 Apr 2019 21:45:06 +0200 Subject: [PATCH 16/17] added search funcionality (#35) * added search functionality * Renamed .json files * Added an empty search response * fix tests that use dynamic url by rewriting their host/scheme/port to hit mockwebserver --- build.gradle | 2 +- library/build.gradle | 8 +- library/dependencies.gradle | 2 +- .../java/io/constructor/core/Constants.kt | 4 + .../java/io/constructor/core/ConstructorIo.kt | 32 +- .../java/io/constructor/data/DataManager.kt | 32 +- .../data/model/search/FacetOption.kt | 3 + .../constructor/data/model/search/Result.kt | 5 + .../data/model/search/ResultData.kt | 12 + .../data/model/search/ResultFacet.kt | 3 + .../data/model/search/SearchData.kt | 6 + .../data/model/search/SearchFacet.kt | 11 + .../data/model/search/SearchGroup.kt | 9 + .../data/model/search/SearchResponse.kt | 5 + .../io/constructor/data/remote/ApiPaths.kt | 3 +- .../constructor/data/remote/ConstructorApi.kt | 14 +- .../constructor/data/DataManagerHttpTest.kt | 98 ++- .../io/constructor/data/DataManagerTest.kt | 57 +- .../io/constructor/util/TestDataLoader.kt | 19 +- ...sponse.json => autocomplete_response.json} | 0 ....json => autocomplete_response_empty.json} | 0 ...mplete_response_with_unexpected_data.json} | 0 .../src/test/resources/search_response.json | 598 +++++++++++++++++ .../test/resources/search_response_empty.json | 24 + .../search_response_unexpected_data.json | 599 ++++++++++++++++++ sample/build.gradle | 3 + .../io/constructor/sample/MainActivity.kt | 5 + sample/src/main/res/layout/activity_main.xml | 10 + 28 files changed, 1517 insertions(+), 47 deletions(-) create mode 100644 library/src/main/java/io/constructor/data/model/search/FacetOption.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/Result.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/ResultData.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/ResultFacet.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SearchData.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SearchFacet.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SearchGroup.kt create mode 100644 library/src/main/java/io/constructor/data/model/search/SearchResponse.kt rename library/src/test/resources/{response.json => autocomplete_response.json} (100%) rename library/src/test/resources/{empty_response.json => autocomplete_response_empty.json} (100%) rename library/src/test/resources/{response_with_unexpected_data.json => autocomplete_response_with_unexpected_data.json} (100%) create mode 100755 library/src/test/resources/search_response.json create mode 100644 library/src/test/resources/search_response_empty.json create mode 100755 library/src/test/resources/search_response_unexpected_data.json diff --git a/build.gradle b/build.gradle index 57530186..c030adcc 100755 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'org.jacoco:org.jacoco.core:0.8.3' + classpath 'org.jacoco:org.jacoco.core:0.8.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } } diff --git a/library/build.gradle b/library/build.gradle index 428af783..82d21b3b 100755 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,7 +8,7 @@ apply plugin: 'jacoco' group='com.github.Constructor-io' jacoco { - toolVersion = '0.8.3' + toolVersion = '0.8.1' } tasks.withType(Test) { @@ -158,12 +158,12 @@ apply from: 'dependencies.gradle' dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'io.reactivex.rxjava2:rxjava:2.1.8' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'io.reactivex.rxjava2:rxjava:2.1.13' implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' testImplementation 'io.mockk:mockk:1.9.kotlin12' testImplementation 'org.robolectric:robolectric:3.6.1' - testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:3.14.1' implementation supportLibs implementation networkLibs implementation otherLibs diff --git a/library/dependencies.gradle b/library/dependencies.gradle index fc99e893..574c9589 100755 --- a/library/dependencies.gradle +++ b/library/dependencies.gradle @@ -2,7 +2,7 @@ ext { versions = [ support : "28.0.0", moshi : "1.5.0", - okHttp : "3.9.0", + okHttp : "3.14.1", retrofit: '2.3.0', dagger : '2.14.1', junit : '4.12', diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index a0e9167b..bf1aafbd 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -27,6 +27,10 @@ class Constants { const val GROUP_DISPLAY_NAME = "group[display_name]" const val USER_ID = "ui" const val TERM_UNKNOWN = "TERM_UNKNOWN" + const val PAGE = "page" + const val PER_PAGE = "num_results_per_page" + const val FILTER_GROUP_ID = "filters[group_id]" + const val FILTER_FACET = "filters[%s]" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index df83ba94..bc6992be 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -8,6 +8,7 @@ import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.Group import io.constructor.data.model.Suggestion +import io.constructor.data.model.search.SearchResponse import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -105,6 +106,25 @@ object ConstructorIo { return dataManager.getAutocompleteResults(query, params.toTypedArray()) } + fun getSearchResults(text: String, vararg facets: Pair>, page: Int? = null, perPage: Int? = null, groupId: Int? = null): Observable> { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + val encodedParams: ArrayList> = arrayListOf() + groupId?.let { encodedParams.add(Constants.QueryConstants.FILTER_GROUP_ID.urlEncode() to it.toString()) } + page?.let { + encodedParams.add(Constants.QueryConstants.PAGE.urlEncode() to page.toString().urlEncode()) + } + perPage?.let { + encodedParams.add(Constants.QueryConstants.PER_PAGE.urlEncode() to perPage.toString().urlEncode()) + } + encodedParams.add(Constants.QueryConstants.SESSION.urlEncode() to sessionId.toString().urlEncode()) + facets.forEach { facet -> + facet.second.forEach { + encodedParams.add(Constants.QueryConstants.FILTER_FACET.format(facet.first).urlEncode() to it.urlEncode()) + } + } + return dataManager.getSearchResults(text, encodedParams = encodedParams.toTypedArray()) + } + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() @@ -121,7 +141,7 @@ object ConstructorIo { }, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("trigger select error: ${t.message}") //To change body of created functions use File | Settings | File Templates. + e("Autocomplete Select event error: ${t.message}") })) } @@ -139,7 +159,7 @@ object ConstructorIo { }, { it.printStackTrace() errorCallback?.invoke(it) - e("trigger search error: ${it.message}") + e("Search Submit event error: ${it.message}") })) } @@ -165,7 +185,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Search result click event error: ${t.message}") + e("Search Result Click event error: ${t.message}") })) } @@ -177,7 +197,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Conversion event error: ${t.message}") + e("Search Results Loaded event error: ${t.message}") })) } @@ -189,7 +209,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Input focus event error: ${t.message}") + e("Input Focus event error: ${t.message}") })) } @@ -203,7 +223,7 @@ object ConstructorIo { .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Input focus event error: ${t.message}") + e("Purchase event error: ${t.message}") })) } diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 80c44395..80fb1c6e 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -1,6 +1,10 @@ package io.constructor.data +import com.squareup.moshi.Moshi +import io.constructor.BuildConfig import io.constructor.data.model.Suggestion +import io.constructor.data.model.search.SearchResponse +import io.constructor.data.remote.ApiPaths import io.constructor.data.remote.ConstructorApi import io.reactivex.Completable import io.reactivex.Observable @@ -9,9 +13,9 @@ import javax.inject.Singleton @Singleton class DataManager @Inject -constructor(private val constructorApi: ConstructorApi) { +constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi) { - fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getSuggestions(text, params.toMap()).map { + fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getAutocompleteResults(text, params.toMap()).map { if (!it.isError) { it.response()?.let { if (it.isSuccessful) { @@ -25,6 +29,30 @@ constructor(private val constructorApi: ConstructorApi) { } }.toObservable() + fun getSearchResults(text: String, encodedParams: Array> = arrayOf()): Observable> { + var dynamicUrl = BuildConfig.BASE_API_URL + "/${ApiPaths.URL_SEARCH.format(text)}" + encodedParams.forEachIndexed { index, pair -> + dynamicUrl += "${if (index != 0) "&" else "?" }${pair.first}=${pair.second}" + } + return constructorApi.getSearchResults(dynamicUrl).map { result -> + if (!result.isError) { + result.response()?.let { + if (it.isSuccessful){ + val adapter = moshi.adapter(SearchResponse::class.java) + val response = it.body()?.string() + val result = response?.let { adapter.fromJson(it) } + result?.rawData = response + ConstructorData.of(result!!) + } else { + ConstructorData.networkError(it.errorBody()?.string()) + } + } ?: ConstructorData.error(result.error()) + } else { + ConstructorData.error(result.error()) + } + }.toObservable() + } + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } diff --git a/library/src/main/java/io/constructor/data/model/search/FacetOption.kt b/library/src/main/java/io/constructor/data/model/search/FacetOption.kt new file mode 100644 index 00000000..3900e7c5 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/FacetOption.kt @@ -0,0 +1,3 @@ +package io.constructor.data.model.search + +data class FacetOption(val count: Int, val value: String?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/Result.kt b/library/src/main/java/io/constructor/data/model/search/Result.kt new file mode 100644 index 00000000..364f7f26 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/Result.kt @@ -0,0 +1,5 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class Result(@Json(name = "data") val result: ResultData, @Json(name = "matched_terms") val matchedTerms: List?, val value: String) diff --git a/library/src/main/java/io/constructor/data/model/search/ResultData.kt b/library/src/main/java/io/constructor/data/model/search/ResultData.kt new file mode 100644 index 00000000..5595199e --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/ResultData.kt @@ -0,0 +1,12 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json +import io.constructor.data.model.Group + +data class ResultData(val description: String?, + val id: String, + @Json(name = "image_url") val imageUrl: String?, + val url: String?, + val facets: List?, + val groups: List?, + var metadata: Map?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt b/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt new file mode 100644 index 00000000..d92e7986 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/ResultFacet.kt @@ -0,0 +1,3 @@ +package io.constructor.data.model.search + +data class ResultFacet(val name: String, val values: List?) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchData.kt b/library/src/main/java/io/constructor/data/model/search/SearchData.kt new file mode 100644 index 00000000..f40a9a98 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchData.kt @@ -0,0 +1,6 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + + +data class SearchData(val facets: List?, val groups: List?, val results: List?, @Json(name = "total_num_results") val resultCount: Int) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt new file mode 100644 index 00000000..fa861f52 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchFacet.kt @@ -0,0 +1,11 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchFacet(val name: String, + @Json(name = "display_name") val displayName: String?, + val status: Map?, + val type: String?, + val min: Int?, + val max: Int?, + val options: List?) diff --git a/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt new file mode 100644 index 00000000..8dbe9fa2 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchGroup.kt @@ -0,0 +1,9 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchGroup(@Json(name = "children") val children: List?, + @Json(name = "parents") val parents: List?, + val count: Int, + @Json(name = "display_name") val displayName: String, + @Json(name = "group_id") val groupId: Long) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt b/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt new file mode 100644 index 00000000..96d4ae86 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/search/SearchResponse.kt @@ -0,0 +1,5 @@ +package io.constructor.data.model.search + +import com.squareup.moshi.Json + +data class SearchResponse(@Json(name = "response") val searchData: SearchData, @Json(name = "result_id") val resultId: String, var rawData: String?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index 2264b226..f55fac04 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -1,7 +1,7 @@ package io.constructor.data.remote object ApiPaths { - const val URL_GET_SUGGESTIONS = "autocomplete/{value}" + const val URL_AUTOCOMPLETE = "autocomplete/{value}" const val URL_AUTOCOMPLETE_SELECT_EVENT = "autocomplete/{term}/select" const val URL_SEARCH_SUBMIT_EVENT = "autocomplete/{term}/search" const val URL_SESSION_START_EVENT = "behavior" @@ -9,5 +9,6 @@ object ApiPaths { const val URL_SEARCH_RESULT_CLICK_EVENT = "autocomplete/{term}/click_through" const val URL_BEHAVIOR = "behavior" const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" + const val URL_SEARCH = "search/%s" } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 128c4381..4cf5dae1 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -4,16 +4,14 @@ import io.constructor.core.Constants import io.constructor.data.model.AutocompleteResult import io.reactivex.Completable import io.reactivex.Single +import okhttp3.ResponseBody import retrofit2.adapter.rxjava2.Result -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query -import retrofit2.http.QueryMap +import retrofit2.http.* interface ConstructorApi { - @GET(ApiPaths.URL_GET_SUGGESTIONS) - fun getSuggestions(@Path("value") value: String, @QueryMap data: Map): Single> + @GET(ApiPaths.URL_AUTOCOMPLETE) + fun getAutocompleteResults(@Path("value") value: String, @QueryMap data: Map): Single> @GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT) fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable @@ -40,4 +38,8 @@ interface ConstructorApi { fun trackPurchase(@Query(Constants.QueryConstants.CUSTOMER_ID) customerIds: List, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable + + @GET + fun getSearchResults(@Url searchUrl: String): Single> + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt index 1c2ad871..76524626 100755 --- a/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerHttpTest.kt @@ -15,6 +15,7 @@ import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk +import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -27,6 +28,9 @@ import retrofit2.converter.moshi.MoshiConverterFactory import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit + + + class DataManagerHttpTest { @Rule @@ -51,8 +55,26 @@ class DataManagerHttpTest { every { configMemoryHolder.testCellParams } returns emptyList() mockServer = MockWebServer() mockServer.start() + val basePath = mockServer.url("" ) + + val client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)) + .addInterceptor { chain -> + var request = chain.request() + if (!request.url().toString().startsWith(basePath.toString())) { + val requestUrl = request.url() + val newRequestUrl = HttpUrl.Builder().scheme(basePath.scheme()) + .encodedQuery(requestUrl.encodedQuery()) + .host(basePath.host()) + .port(basePath.port()) + .encodedPath(requestUrl.encodedPath()).build() + request = request.newBuilder() + .url(newRequestUrl) + .build() + } + chain.proceed(request) + } + .readTimeout(4, TimeUnit.SECONDS).build() - val client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).readTimeout(4, TimeUnit.SECONDS).build() val moshi = Moshi .Builder() @@ -61,20 +83,20 @@ class DataManagerHttpTest { // Get an instance of Retrofit val retrofit = Retrofit.Builder() - .baseUrl(mockServer.url("").toString()) + .baseUrl(basePath.toString()) .client(client) .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() constructorApi = retrofit.create(ConstructorApi::class.java) - dataManager = DataManager(constructorApi) + dataManager = DataManager(constructorApi, moshi) } @Test fun getAutocompleteResults() { - val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response.json")) + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response.json")) mockServer.enqueue(mockResponse) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { @@ -86,7 +108,7 @@ class DataManagerHttpTest { @Test fun getAutocompleteResultsBadServerResponse() { - val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") mockServer.enqueue(mockResponse) val observer = dataManager.getAutocompleteResults("titanic").test() @@ -99,8 +121,8 @@ class DataManagerHttpTest { @Test fun getAutocompleteResultsTimeoutException() { - val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response.json")) + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response.json")) mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) mockServer.enqueue(mockResponse) val observer = dataManager.getAutocompleteResults("titanic").test() @@ -113,8 +135,8 @@ class DataManagerHttpTest { @Test fun getAutocompleteResultsUnexpectedDataResponse() { - val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("response_with_unexpected_data.json")) + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_with_unexpected_data.json")) mockServer.enqueue(mockResponse) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { @@ -126,8 +148,8 @@ class DataManagerHttpTest { @Test fun getAutocompleteResultsEmptyResponse() { - val path = "/" + ApiPaths.URL_GET_SUGGESTIONS.replace("{value}", "titanic") - val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("empty_response.json")) + val path = "/" + ApiPaths.URL_AUTOCOMPLETE.replace("{value}", "titanic") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("autocomplete_response_empty.json")) mockServer.enqueue(mockResponse) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { @@ -436,4 +458,56 @@ class DataManagerHttpTest { assert(request.path.contains("${Constants.QueryConstants.CUSTOMER_ID}=2")) } + @Test + fun getSearchResult() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.results!!.size == 20 + } + } + + @Test + fun getSearchResultsBadServerResponse() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.networkError + } + } + + @Test + fun getSearchResultsTimeoutException() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response.json")) + mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.isError + } + } + + @Test + fun getSearchUnexpectedDataResponse() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_unexpected_data.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.resultCount == 23 + } + } + + @Test + fun getSearchResultsEmptyResponse() { + val path = "/" + ApiPaths.URL_SEARCH.format("corn") + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("search_response_empty.json")) + mockServer.enqueue(mockResponse) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.results!!.isEmpty() + } + } + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index b834292b..6a65a4d8 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -1,5 +1,7 @@ package io.constructor.data +import com.squareup.moshi.KotlinJsonAdapterFactory +import com.squareup.moshi.Moshi import io.constructor.data.model.AutocompleteResult import io.constructor.data.remote.ConstructorApi import io.constructor.util.RxSchedulersOverrideRule @@ -23,11 +25,16 @@ class DataManagerTest { private var constructorApi = mockk() - private var dataManager = DataManager(constructorApi) + private var moshi = Moshi + .Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + private var dataManager = DataManager(constructorApi, moshi) @Test - fun getSuggestions() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) + fun getAutocompleteResults() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -35,8 +42,8 @@ class DataManagerTest { } @Test - fun getSuggestionsBadServerResponse() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + fun getAutocompleteResultsBadServerResponse() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.networkError @@ -44,8 +51,8 @@ class DataManagerTest { } @Test - fun getSuggestionsException() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.error(Exception())) + fun getAutocompleteResultsException() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.error(Exception())) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isError @@ -53,8 +60,8 @@ class DataManagerTest { } @Test - fun getSuggestionsUnexpectedDataResponse() { - every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) + fun getAutocompleteResultsUnexpectedDataResponse() { + every { constructorApi.getAutocompleteResults("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -62,8 +69,8 @@ class DataManagerTest { } @Test - fun getSuggestionsEmptyResponse() { - every { constructorApi.getSuggestions("titanic", any() + fun getAutocompleteResultsEmptyResponse() { + every { constructorApi.getAutocompleteResults("titanic", any() ) } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { @@ -191,4 +198,32 @@ class DataManagerTest { verify(exactly = 1) { constructorApi.trackPurchase(any(), any(), any()) } } + @Test + fun getSearchResults() { + val rb = ResponseBody.create(MediaType.get("application/json"), TestDataLoader.loadAsString("search_response.json")) + every { constructorApi.getSearchResults(any()) } returns Single.just(Result.response(Response.success(rb))) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.get()!!.searchData.resultCount == 23 + } + } + + @Test + fun getSearchResultsBadServerResponse() { + every { constructorApi.getSearchResults("https://ac.cnstrc.com/search/corn") } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.networkError + } + } + + @Test + fun getSearchResultsException() { + every { constructorApi.getSearchResults("https://ac.cnstrc.com/search/corn") } returns Single.just(Result.error(Exception())) + val observer = dataManager.getSearchResults("corn").test() + observer.assertComplete().assertValue { + it.isError + } + } + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/util/TestDataLoader.kt b/library/src/test/java/io/constructor/util/TestDataLoader.kt index 140a0693..684130c0 100755 --- a/library/src/test/java/io/constructor/util/TestDataLoader.kt +++ b/library/src/test/java/io/constructor/util/TestDataLoader.kt @@ -3,6 +3,7 @@ package io.constructor.util import com.squareup.moshi.KotlinJsonAdapterFactory import com.squareup.moshi.Moshi import io.constructor.data.model.AutocompleteResult +import io.constructor.data.model.search.SearchResponse import okio.Buffer import java.io.File import java.io.FileInputStream @@ -12,11 +13,11 @@ import java.nio.charset.Charset object TestDataLoader { - fun loadResponse() : AutocompleteResult? = loadResult("response.json") + fun loadResponse() : AutocompleteResult? = loadResult("autocomplete_response.json") - fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("response_with_unexpected_data.json") + fun loadResponseWithUnexpectedData() : AutocompleteResult? = loadResult("autocomplete_response_with_unexpected_data.json") - fun loadEmptyResponse() : AutocompleteResult? = loadResult("empty_response.json") + fun loadEmptyResponse() : AutocompleteResult? = loadResult("autocomplete_response_empty.json") private fun loadResult(fileName: String): AutocompleteResult? { val file = File(TestDataLoader::class.java.classLoader.getResource(fileName).path) @@ -31,6 +32,18 @@ object TestDataLoader { return result } + private fun convertToSearchResult(stringResponse: String): SearchResponse? { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val jsonAdapter = moshi.adapter(SearchResponse::class.java) + var result: SearchResponse? = null + try { + result = jsonAdapter.fromJson(stringResponse) + } catch (e: IOException) { + e.printStackTrace() + } + return result + } + fun loadAsString(fileName: String): String { var result = "" try { diff --git a/library/src/test/resources/response.json b/library/src/test/resources/autocomplete_response.json similarity index 100% rename from library/src/test/resources/response.json rename to library/src/test/resources/autocomplete_response.json diff --git a/library/src/test/resources/empty_response.json b/library/src/test/resources/autocomplete_response_empty.json similarity index 100% rename from library/src/test/resources/empty_response.json rename to library/src/test/resources/autocomplete_response_empty.json diff --git a/library/src/test/resources/response_with_unexpected_data.json b/library/src/test/resources/autocomplete_response_with_unexpected_data.json similarity index 100% rename from library/src/test/resources/response_with_unexpected_data.json rename to library/src/test/resources/autocomplete_response_with_unexpected_data.json diff --git a/library/src/test/resources/search_response.json b/library/src/test/resources/search_response.json new file mode 100755 index 00000000..1cce0cbe --- /dev/null +++ b/library/src/test/resources/search_response.json @@ -0,0 +1,598 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "corn" + }, + "response": { + "facets": [], + "groups": [ + { + "children": [], + "count": 9, + "display_name": "Horror", + "group_id": "27", + "parents": [] + }, + { + "children": [], + "count": 7, + "display_name": "Thriller", + "group_id": "53", + "parents": [] + }, + { + "children": [], + "count": 4, + "display_name": "Drama", + "group_id": "18", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Documentary", + "group_id": "99", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Mystery", + "group_id": "9648", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Fantasy", + "group_id": "14", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Animation", + "group_id": "16", + "parents": [] + }, + { + "children": [], + "count": 6, + "display_name": "Comedy", + "group_id": "35", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Crime", + "group_id": "80", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Action", + "group_id": "28", + "parents": [] + }, + { + "children": [], + "count": 2, + "display_name": "Romance", + "group_id": "10749", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Science Fiction", + "group_id": "878", + "parents": [] + } + ], + "results": [ + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iv: the gathering", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/tRjeV9AZgCXGTqyvlp7Ui55Yb3l.jpg", + "url": "https://www.top250.tv/movies/25750" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn IV: The Gathering" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + }, + { + "display_name": "Fantasy", + "group_id": "14", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zH77CDSRPeYfZZJyyKSt84j62m8.jpg", + "url": "https://www.top250.tv/movies/10823" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn ii: the final sacrifice", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/lqFb8Mnx9tFPUevnfbz9o2adLFw.jpg", + "url": "https://www.top250.tv/movies/25748" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn II: The Final Sacrifice" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iii: urban harvest", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/Ajp5lVNAW0Kfi3uUlCCpIri28B8.jpg", + "url": "https://www.top250.tv/movies/25749" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn III: Urban Harvest" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the corn is green", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zBN62KUP4WGys96vmJUFRKc43B9.jpg", + "url": "https://www.top250.tv/movies/43492" + }, + "matched_terms": [ + "corn" + ], + "value": "The Corn Is Green" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "king corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pvqjdmu5IdzUPQgqwylpPrUSKSd.jpg", + "url": "https://www.top250.tv/movies/15281" + }, + "matched_terms": [ + "corn" + ], + "value": "King Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "corn island", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zM4ZZ7IpKQA266ynNWOvn3LfKE.jpg", + "url": "https://www.top250.tv/movies/282376" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn Island" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Mystery", + "group_id": "9648", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn 666: isaac's return", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/cAoLi0dxRZwA20LUzTTGN3Xn39Y.jpg", + "url": "https://www.top250.tv/movies/25752" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn 666: Isaac's Return" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn v: fields of terror", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/w3ZOi0jbHNEQ26MEt1X3XCJzBYe.jpg", + "url": "https://www.top250.tv/movies/25751" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn V: Fields of Terror" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: genesis", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/94Cc2YJMsCtezYRcmL1PyBNhE1y.jpg", + "url": "https://www.top250.tv/movies/70575" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Genesis" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: revelation", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/sL3ZaPFwgkfn9KuIsh861zsPX0Y.jpg", + "url": "https://www.top250.tv/movies/25753" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Revelation" + }, + { + "data": { + "groups": [ + { + "display_name": "Animation", + "group_id": "16", + "path": null, + "path_list": [] + } + ], + "id": "corn on the cop", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/1ifAyPrAJW4WDVqmvBmZo3hDhrD.jpg", + "url": "https://www.top250.tv/movies/234377" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn on the Cop" + }, + { + "data": { + "groups": [ + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "con air", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/yhaOQ7xXw0PLHLvg1w0M9zlPdg6.jpg", + "url": "https://www.top250.tv/movies/1701" + }, + "matched_terms": [ + "con" + ], + "value": "Con Air" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Science Fiction", + "group_id": "878", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + } + ], + "id": "def-con 4", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/wmCLwtkwzrrphDhR1smtFLZrQxG.jpg", + "url": "https://www.top250.tv/movies/42033" + }, + "matched_terms": [ + "con" + ], + "value": "Def-Con 4" + }, + { + "data": { + "groups": [ + { + "display_name": "Romance", + "group_id": "10749", + "path": null, + "path_list": [] + }, + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + }, + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the con", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/fDcPgFzVioueAcp13nLw8TGiVOC.jpg", + "url": "https://www.top250.tv/movies/131729" + }, + "matched_terms": [ + "con" + ], + "value": "The Con" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "comic-con episode iv: a fan's hope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/axltQJKHxolLfbGwuTKZZlLbBsZ.jpg", + "url": "https://www.top250.tv/movies/91356" + }, + "matched_terms": [ + "con" + ], + "value": "Comic-Con Episode IV: A Fan's Hope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "chronic-con, episode 420: a new dope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/16pPnt4ce0i3zU7QOoHDD4JN9Oe.jpg", + "url": "https://www.top250.tv/movies/347528" + }, + "matched_terms": [ + "con" + ], + "value": "Chronic-Con, Episode 420: A New Dope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "vaya con dios", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pIV1xgavkLYL33agVmRxpwq9CE4.jpg", + "url": "https://www.top250.tv/movies/6318" + }, + "matched_terms": [ + "con" + ], + "value": "Vaya con Dios" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "the con artists", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/rn2xTdw2pyRTcjcwHy5yEfxkfyQ.jpg", + "url": "https://www.top250.tv/movies/300433" + }, + "matched_terms": [ + "con" + ], + "value": "The Con Artists" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "tempo instabile con probabili schiarite", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/iaOhlyHXqAIhsoGXqD9S4I066zo.jpg", + "url": "https://www.top250.tv/movies/333888" + }, + "matched_terms": [ + "con" + ], + "value": "Tempo instabile con probabili schiarite" + } + ], + "sort_options": [], + "total_num_results": 23 + }, + "result_id": "5b19c365-1c85-4c2d-b627-0509d60ce2d5" +} diff --git a/library/src/test/resources/search_response_empty.json b/library/src/test/resources/search_response_empty.json new file mode 100644 index 00000000..5f82e2cc --- /dev/null +++ b/library/src/test/resources/search_response_empty.json @@ -0,0 +1,24 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "cornucopiasofcorndogs" + }, + "response": { + "facets": [], + "groups": [], + "results": [], + "sort_options": [], + "total_num_results": 0 + }, + "result_id": "2ea93527-91d6-4dfa-86b0-6a0e8158bfd1" +} diff --git a/library/src/test/resources/search_response_unexpected_data.json b/library/src/test/resources/search_response_unexpected_data.json new file mode 100755 index 00000000..7275c3f9 --- /dev/null +++ b/library/src/test/resources/search_response_unexpected_data.json @@ -0,0 +1,599 @@ +{ + "request": { + "ef-11": "22", + "ef-ab": "cd", + "fmt_options": { + "groups_max_depth": 1, + "groups_start": "current" + }, + "num_results_per_page": 20, + "page": 1, + "section": "Products", + "sort_by": "relevance", + "sort_order": "descending", + "term": "corn" + }, + "response": { + "facets": [], + "groups": [ + { + "children": [], + "count": 9, + "display_name": "Horror", + "group_id": "27", + "parents": [] + }, + { + "children": [], + "count": 7, + "display_name": "Thriller", + "group_id": "53", + "parents": [], + "unknown_name": "New name" + }, + { + "children": [], + "count": 4, + "display_name": "Drama", + "group_id": "18", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Documentary", + "group_id": "99", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Mystery", + "group_id": "9648", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Fantasy", + "group_id": "14", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Animation", + "group_id": "16", + "parents": [] + }, + { + "children": [], + "count": 6, + "display_name": "Comedy", + "group_id": "35", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Crime", + "group_id": "80", + "parents": [] + }, + { + "children": [], + "count": 3, + "display_name": "Action", + "group_id": "28", + "parents": [] + }, + { + "children": [], + "count": 2, + "display_name": "Romance", + "group_id": "10749", + "parents": [] + }, + { + "children": [], + "count": 1, + "display_name": "Science Fiction", + "group_id": "878", + "parents": [] + } + ], + "results": [ + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iv: the gathering", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/tRjeV9AZgCXGTqyvlp7Ui55Yb3l.jpg", + "url": "https://www.top250.tv/movies/25750" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn IV: The Gathering" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + }, + { + "display_name": "Fantasy", + "group_id": "14", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zH77CDSRPeYfZZJyyKSt84j62m8.jpg", + "url": "https://www.top250.tv/movies/10823" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn ii: the final sacrifice", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/lqFb8Mnx9tFPUevnfbz9o2adLFw.jpg", + "url": "https://www.top250.tv/movies/25748" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn II: The Final Sacrifice" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn iii: urban harvest", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/Ajp5lVNAW0Kfi3uUlCCpIri28B8.jpg", + "url": "https://www.top250.tv/movies/25749" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn III: Urban Harvest" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the corn is green", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zBN62KUP4WGys96vmJUFRKc43B9.jpg", + "url": "https://www.top250.tv/movies/43492" + }, + "matched_terms": [ + "corn" + ], + "value": "The Corn Is Green" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "king corn", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pvqjdmu5IdzUPQgqwylpPrUSKSd.jpg", + "url": "https://www.top250.tv/movies/15281" + }, + "matched_terms": [ + "corn" + ], + "value": "King Corn" + }, + { + "data": { + "groups": [ + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "corn island", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/zM4ZZ7IpKQA266ynNWOvn3LfKE.jpg", + "url": "https://www.top250.tv/movies/282376" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn Island" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Mystery", + "group_id": "9648", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn 666: isaac's return", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/cAoLi0dxRZwA20LUzTTGN3Xn39Y.jpg", + "url": "https://www.top250.tv/movies/25752" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn 666: Isaac's Return" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn v: fields of terror", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/w3ZOi0jbHNEQ26MEt1X3XCJzBYe.jpg", + "url": "https://www.top250.tv/movies/25751" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn V: Fields of Terror" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: genesis", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/94Cc2YJMsCtezYRcmL1PyBNhE1y.jpg", + "url": "https://www.top250.tv/movies/70575" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Genesis" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + } + ], + "id": "children of the corn: revelation", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/sL3ZaPFwgkfn9KuIsh861zsPX0Y.jpg", + "url": "https://www.top250.tv/movies/25753" + }, + "matched_terms": [ + "corn" + ], + "value": "Children of the Corn: Revelation" + }, + { + "data": { + "groups": [ + { + "display_name": "Animation", + "group_id": "16", + "path": null, + "path_list": [] + } + ], + "id": "corn on the cop", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/1ifAyPrAJW4WDVqmvBmZo3hDhrD.jpg", + "url": "https://www.top250.tv/movies/234377" + }, + "matched_terms": [ + "corn" + ], + "value": "Corn on the Cop" + }, + { + "data": { + "groups": [ + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "con air", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/yhaOQ7xXw0PLHLvg1w0M9zlPdg6.jpg", + "url": "https://www.top250.tv/movies/1701" + }, + "matched_terms": [ + "con" + ], + "value": "Con Air" + }, + { + "data": { + "groups": [ + { + "display_name": "Horror", + "group_id": "27", + "path": null, + "path_list": [] + }, + { + "display_name": "Science Fiction", + "group_id": "878", + "path": null, + "path_list": [] + }, + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + } + ], + "id": "def-con 4", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/wmCLwtkwzrrphDhR1smtFLZrQxG.jpg", + "url": "https://www.top250.tv/movies/42033" + }, + "matched_terms": [ + "con" + ], + "value": "Def-Con 4" + }, + { + "data": { + "groups": [ + { + "display_name": "Romance", + "group_id": "10749", + "path": null, + "path_list": [] + }, + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + }, + { + "display_name": "Drama", + "group_id": "18", + "path": null, + "path_list": [] + } + ], + "id": "the con", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/fDcPgFzVioueAcp13nLw8TGiVOC.jpg", + "url": "https://www.top250.tv/movies/131729" + }, + "matched_terms": [ + "con" + ], + "value": "The Con" + }, + { + "data": { + "groups": [ + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "comic-con episode iv: a fan's hope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/axltQJKHxolLfbGwuTKZZlLbBsZ.jpg", + "url": "https://www.top250.tv/movies/91356" + }, + "matched_terms": [ + "con" + ], + "value": "Comic-Con Episode IV: A Fan's Hope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + }, + { + "display_name": "Documentary", + "group_id": "99", + "path": null, + "path_list": [] + } + ], + "id": "chronic-con, episode 420: a new dope", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/16pPnt4ce0i3zU7QOoHDD4JN9Oe.jpg", + "url": "https://www.top250.tv/movies/347528" + }, + "matched_terms": [ + "con" + ], + "value": "Chronic-Con, Episode 420: A New Dope" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "vaya con dios", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/pIV1xgavkLYL33agVmRxpwq9CE4.jpg", + "url": "https://www.top250.tv/movies/6318" + }, + "matched_terms": [ + "con" + ], + "value": "Vaya con Dios" + }, + { + "data": { + "groups": [ + { + "display_name": "Thriller", + "group_id": "53", + "path": null, + "path_list": [] + }, + { + "display_name": "Action", + "group_id": "28", + "path": null, + "path_list": [] + }, + { + "display_name": "Crime", + "group_id": "80", + "path": null, + "path_list": [] + } + ], + "id": "the con artists", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/rn2xTdw2pyRTcjcwHy5yEfxkfyQ.jpg", + "url": "https://www.top250.tv/movies/300433" + }, + "matched_terms": [ + "con" + ], + "value": "The Con Artists" + }, + { + "data": { + "groups": [ + { + "display_name": "Comedy", + "group_id": "35", + "path": null, + "path_list": [] + } + ], + "id": "tempo instabile con probabili schiarite", + "image_url": "https://image.tmdb.org/t/p/w185_and_h278_bestv2/iaOhlyHXqAIhsoGXqD9S4I066zo.jpg", + "url": "https://www.top250.tv/movies/333888" + }, + "matched_terms": [ + "con" + ], + "value": "Tempo instabile con probabili schiarite" + } + ], + "sort_options": [], + "total_num_results": 23 + }, + "result_id": "5b19c365-1c85-4c2d-b627-0509d60ce2d5" +} diff --git a/sample/build.gradle b/sample/build.gradle index 35ddde42..c99a9db0 100755 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -38,6 +38,9 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:recyclerview-v7:28.0.0' implementation "com.android.support:cardview-v7:28.0.0" + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'io.reactivex.rxjava2:rxjava:2.1.13' + implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' } diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 0a963b54..de7a872a 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import io.constructor.core.ConstructorIo +import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.* @@ -18,5 +19,9 @@ class MainActivity : AppCompatActivity() { button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.0) } button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } + button6.setOnClickListener { ConstructorIo.getSearchResults("corn").subscribeOn(Schedulers.io()).subscribe { + + } } + } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index ea623bb5..03103d6d 100755 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -55,4 +55,14 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button4" /> + +