From a4a2ce417e8dc4a39b0d855ac7a332c579020d3b Mon Sep 17 00:00:00 2001 From: Cristi Dregan Date: Tue, 22 Jun 2021 14:02:07 +0300 Subject: [PATCH] Twelfth release --- .idea/runConfigurations.xml | 12 -- OmetriaSDK/build.gradle | 6 +- .../com/android/ometriasdk/core/Ometria.kt | 16 ++- .../com/android/ometriasdk/core/Repository.kt | 30 +++++ .../core/listener/ProcessAppLinkListener.kt | 20 ++++ .../core/network/ConnectionFactory.kt | 3 +- .../OmetriaActivityLifecycleHelper.kt | 3 +- .../notification/NotificationHandler.kt | 4 +- .../notification/OmetriaPushNotification.kt | 5 +- README.md | 112 ++++++++++++++++++ app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 11 ++ .../main/java/com/android/sample/SampleApp.kt | 4 +- .../sample/presentation/MainActivity.kt | 45 ++++++- .../ic_notification_nys.xml | 15 +++ .../res/drawable-hdpi/ic_notification_nys.png | Bin 0 -> 373 bytes .../res/drawable-mdpi/ic_notification_nys.png | Bin 0 -> 265 bytes .../drawable-xhdpi/ic_notification_nys.png | Bin 0 -> 478 bytes .../drawable-xxhdpi/ic_notification_nys.png | Bin 0 -> 673 bytes app/src/main/res/values/strings.xml | 2 + build.gradle | 2 +- 21 files changed, 268 insertions(+), 28 deletions(-) delete mode 100644 .idea/runConfigurations.xml create mode 100644 OmetriaSDK/src/main/java/com/android/ometriasdk/core/listener/ProcessAppLinkListener.kt create mode 100644 app/src/main/res/drawable-anydpi-v24/ic_notification_nys.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_notification_nys.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification_nys.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_nys.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_nys.png diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/OmetriaSDK/build.gradle b/OmetriaSDK/build.gradle index 2b0e604..01720c0 100644 --- a/OmetriaSDK/build.gradle +++ b/OmetriaSDK/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 29 versionCode 1 - versionName "1.1.2" + versionName "1.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -28,8 +28,8 @@ dependencies { // Android Core implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.appcompat:appcompat:1.3.0' // Lifecycle implementation 'androidx.lifecycle:lifecycle-process:2.3.1' diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt index e59d44d..f90c1c2 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt @@ -1,6 +1,7 @@ package com.android.ometriasdk.core import android.app.Application +import android.app.Notification.COLOR_DEFAULT import android.content.Intent import android.net.Uri import androidx.core.app.NotificationManagerCompat @@ -26,6 +27,7 @@ import com.android.ometriasdk.core.Constants.Params.PUSH_TOKEN import com.android.ometriasdk.core.event.EventHandler import com.android.ometriasdk.core.event.OmetriaBasket import com.android.ometriasdk.core.event.OmetriaEventType +import com.android.ometriasdk.core.listener.ProcessAppLinkListener import com.android.ometriasdk.core.network.Client import com.android.ometriasdk.core.network.ConnectionFactory import com.android.ometriasdk.core.network.OmetriaThreadPoolExecutor @@ -78,7 +80,8 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { fun initialize( application: Application, apiToken: String, - notificationIcon: Int + notificationIcon: Int, + notificationColor: Int? = COLOR_DEFAULT ) = instance.also { it.ometriaConfig = OmetriaConfig(apiToken, application) it.localCache = LocalCache(application) @@ -90,7 +93,7 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { ) it.eventHandler = EventHandler(application, it.repository) it.notificationHandler = - NotificationHandler(application, notificationIcon, it.executor) + NotificationHandler(application, notificationIcon, notificationColor, it.executor) it.isInitialized = true it.notificationInteractionHandler = instance @@ -389,6 +392,15 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { localCache.clearEvents() } + /** + * Retrieves the redirect url for the url that you provide. + * @param url The url that will be processed. + * @param listener The callback interface. + */ + fun processAppLink(url: String, listener: ProcessAppLinkListener) { + repository.getRedirectForUrl(url, listener) + } + override fun onDeepLinkInteraction(deepLink: String) { Logger.d(Constants.Logger.PUSH_NOTIFICATIONS, "Open URL: $deepLink") val intent = Intent(Intent.ACTION_VIEW) diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt index d367f8f..4ba663c 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt @@ -1,12 +1,17 @@ package com.android.ometriasdk.core +import android.os.Handler import com.android.ometriasdk.core.Constants.Logger.NETWORK import com.android.ometriasdk.core.event.OmetriaEvent import com.android.ometriasdk.core.event.toApiRequest +import com.android.ometriasdk.core.listener.ProcessAppLinkListener import com.android.ometriasdk.core.network.Client import com.android.ometriasdk.core.network.OmetriaThreadPoolExecutor import com.android.ometriasdk.core.network.model.OmetriaApiRequest import java.io.IOException +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URL /** * Created by cristiandregan @@ -21,6 +26,7 @@ internal class Repository( private val executor: OmetriaThreadPoolExecutor ) { + private val resultHandler: Handler = Handler() private val dropStatusCodesRange = 400..499 fun flushEvents(events: List, success: () -> Unit, error: () -> Unit) { @@ -126,4 +132,28 @@ internal class Repository( fun areNotificationsEnabled(): Boolean { return localCache.areNotificationsEnabled() } + + fun getRedirectForUrl(url: String, listener: ProcessAppLinkListener) { + executor.execute { + var urlTemp: URL? = null + var connection: HttpURLConnection? = null + try { + urlTemp = URL(url) + } catch (e: MalformedURLException) { + listener.onProcessFailed(e.message ?: "Something went wrong") + } + try { + connection = urlTemp?.openConnection() as HttpURLConnection + } catch (e: IOException) { + listener.onProcessFailed(e.message ?: "Something went wrong") + } + try { + connection?.responseCode + } catch (e: IOException) { + listener.onProcessFailed(e.message ?: "Something went wrong") + } + resultHandler.post { listener.onProcessResult(connection?.url.toString()) } + connection?.disconnect() + } + } } \ No newline at end of file diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/listener/ProcessAppLinkListener.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/listener/ProcessAppLinkListener.kt new file mode 100644 index 0000000..59ca057 --- /dev/null +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/listener/ProcessAppLinkListener.kt @@ -0,0 +1,20 @@ +package com.android.ometriasdk.core.listener + +/** + * A callback interface for processing an App Link URL. + * Processing is done async. Results will be returned on Main Thread. + */ +interface ProcessAppLinkListener { + + /** + * Called when a process completes successfully. + * @param url The redirect URL resulted from process. + */ + fun onProcessResult(url: String) + + /** + * Called when an exception occurs during a process. + * @param error The error message + */ + fun onProcessFailed(error: String) +} \ No newline at end of file diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/network/ConnectionFactory.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/network/ConnectionFactory.kt index 65ef421..2015c68 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/network/ConnectionFactory.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/network/ConnectionFactory.kt @@ -25,8 +25,7 @@ internal class ConnectionFactory(private val ometriaConfig: OmetriaConfig) { fun postConnection(): HttpURLConnection { val url = API_ENDPOINT - val requestedURL: URL - requestedURL = try { + val requestedURL: URL = try { URL(url) } catch (e: MalformedURLException) { throw IOException("Attempted to use malformed url: $url", e) diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/lifecycle/OmetriaActivityLifecycleHelper.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/lifecycle/OmetriaActivityLifecycleHelper.kt index 569a069..ea340db 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/lifecycle/OmetriaActivityLifecycleHelper.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/lifecycle/OmetriaActivityLifecycleHelper.kt @@ -90,7 +90,8 @@ internal class OmetriaActivityLifecycleHelper( * Using activity started callback to track Screen View event */ override fun onActivityStarted(activity: Activity) { - Ometria.instance().trackAutomatedScreenViewedEvent(activity::class.simpleName) + // Disabled at clients request +// Ometria.instance().trackAutomatedScreenViewedEvent(activity::class.simpleName) } /** diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/NotificationHandler.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/NotificationHandler.kt index c0afa52..f34dc5c 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/NotificationHandler.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/NotificationHandler.kt @@ -24,12 +24,14 @@ const val KEY_OMETRIA = "ometria" internal class NotificationHandler( context: Context, notificationIcon: Int, + notificationColor: Int?, private val executor: OmetriaThreadPoolExecutor, ) { private val ometriaPushNotification: OmetriaPushNotification = OmetriaPushNotification( context, - notificationIcon + notificationIcon, + notificationColor ) /** diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/OmetriaPushNotification.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/OmetriaPushNotification.kt index 07822c5..baf9fe2 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/OmetriaPushNotification.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/notification/OmetriaPushNotification.kt @@ -1,5 +1,6 @@ package com.android.ometriasdk.notification +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -22,7 +23,8 @@ const val OMETRIA_CHANNEL_NAME = "ometria" internal class OmetriaPushNotification( private val context: Context, - private val notificationIcon: Int + private val notificationIcon: Int, + private val notificationColor: Int? ) { fun createPushNotification( @@ -46,6 +48,7 @@ internal class OmetriaPushNotification( .setAutoCancel(true) .setContentIntent(contentIntent) .setLargeIcon(image) + .setColor(notificationColor ?: Notification.COLOR_DEFAULT) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/README.md b/README.md index 9c0d76e..c6d7856 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,31 @@ Check with the marketing team about the specifics, and what they might need. Esp trackCustomEvent(customEventType: String, additionalInfo: Map = mapOf()) ``` +### `OmetriaBasket` + +An object that describes the contents of a shopping basket. + +#### Properties + +* `currency`: (`String`, required) - A string representing the currency in ISO currency format. e.g. `"USD"`, `"GBP"`. +* `price`: (`float`, required) - A float value representing the pricing. +* `items`: (`Array[OmetriaBasketItem]`) - An array containing the item entries in this basket. +* `link`: (`String`) - A deeplink to the web or in-app page for this basket. Can be used ina notification sent to the user, e.g. "Forgot to check out? Here's + your basket to continue: ". Following that link should take them straight to the basket page. + +### `OmetriaBasketItem` + +An object that describes the contents of a shopping basket. + +It can have its own price and quantity based on different rules and promotions that are being applied. + +#### Properties + +* `productId`: (`String`, required) - A string representing the unique identifier of this product. +* `sku`: (`String`, optional) - A string representing the stock keeping unit, which allows identifying a particular item. +* `quantity`: (`Int`, required) - The number of items that this entry represents. +* `price`: (`Float`, required) - Float value representing the price for one item. The currency is established by the OmetriaBasket containing this item. + ### Automatically tracked events The following events are automatically tracked by the SDK. @@ -435,3 +460,90 @@ class SampleApp : Application(), OmetriaNotificationInteractionHandler { } } ``` + +7\. App links guide +---------------------------- + +Ometria sends personalised emails with URLs that point back to your website. In order to open these URLs inside your application, make sure you follow this guide. + +### Pre-requisites + +First, make sure you have an SSL-enabled Ometria tracking domain set up for your account. You may already have this for +your email campaigns, but if not ask your Ometria contact to set one up, and they should provide you with the domain. + +### Handle App Links inside your application + +To add Android App Links to your app, define intent filters that open your app content using HTTP URLs. Intent filters for incoming links +will be added inside your **AndroidManifest** file, the following XML snippet is an example (assuming "clickom.omdemo.net" is the tracking domain): + +```xml + + + + + + + + +``` + +This will ensure that when your customers click on links in Ometria emails, your app opens instead of the browser. + +**Note:** This does not associate your website's domain with the app, only the tracking domain. + +Find more about Android App Links [here](https://developer.android.com/training/app-links/verify-site-associations). + +### Create a digital asset links JSON file and send it to your Ometria contact. + +The Digital Asset Links JSON file is used to create a relationship between a domain and your app. [You can find more info about it here](https://developer.android.com/training/app-links/verify-site-associations#web-assoc). +A basic example should look like this: + +```javascript +[ + { + "relation": [ "delegate_permission/common.handle_all_urls" ], + "target": { + "namespace": "android_app", + "package_name": "com.android.sample", + "sha256_cert_fingerprints": ["51:5B:24:4B:C7:A4:7F:D2:FA:B2:C7:23:73:7A:A0:91:A5:B6:29:49:08:73:E1:51:7E:CF:60:28:53:65:47:25"] + } + } +] +``` + +Save it and name it "assetlinks.json". Then send it to your Ometria contact - we will upload this for you so that it will be available behind the +tracking domain. + +### Process App Links inside your application + +The final step is to process the URLs in your app and take the user to the appropriate sections of the app. Note that +you need to implement the mapping between your website's URLs and the screens of your app. + +See also [Linking push notifications to app screens](https://support.ometria.com/hc/en-gb/articles/4402644059793-Linking-push-notifications-to-app-screens). + +If you are dealing with normal URLs pointing to your website, you can decompose it into different path components and parameters. This will allow you to source the required information to navigate through to the correct screen in your app. + +However, Ometria emails contain obfuscated tracking URLs, and these need to be converted back to the original URL, pointing to your website, before you can map the URL to an app screen. To do this, the SDK provides a method called `processAppLink`: + +```kotlin +private fun handleAppLinkFromIntent() { + // you can check here whether the URL is one that you can handle without converting it back + intent.dataString?.let { url -> + Ometria.instance().processAppLink(url, object : ProcessAppLinkListener { + override fun onProcessResult(url: String) { + // you can now handle the retrieved url as you would any other url from your website + } + + override fun onProcessFailed(error: String) { + // an error may have occurred + } + }) + } +} +``` + +**Warning**: The method above runs asynchronously. Depending on the Internet speed on the device, processing time can vary. For best results, you could implement a loading state that is displayed while the URL is being processed. + +If you have done everything correctly, the app should now be able to open app links and allow you to handle them inside the app. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 39f34d2..3b0b466 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,10 +55,10 @@ dependencies { // Support libraries implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.4.0-beta01' + implementation 'com.google.android.material:material:1.4.0-rc01' // Add the SDK for Firebase Cloud Messaging implementation 'com.google.firebase:firebase-messaging:22.0.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c38250..7f4cae2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,17 @@ + + + + + + + + + + bottomMenuBnv.setOnItemSelectedListener { item -> when (item.itemId) { R.id.restaurants -> { switchFragment(FIRST_FRAGMENT_POS) @@ -64,4 +72,39 @@ class MainActivity : AppCompatActivity() { } }) } + + private fun handleAppLinkFromIntent() { + // Here you should check whether the link is one that can already be handled by the app. + // If the link is identified as one coming from an Ometria campaign, you will be able to get the final URL + // by calling the processAppLink method. + // The processing is done async, so you should present a loading screen. + intent.dataString?.let { url -> + Ometria.instance().processAppLink(url, object : ProcessAppLinkListener { + override fun onProcessResult(url: String) { + displayRedirectUrlDialog(url) + } + + override fun onProcessFailed(error: String) { + displayRedirectUrlDialog(error) + } + }) + } + } + + private fun displayRedirectUrlDialog(message: String) { + val messageSpannableString = SpannableString(message) + Linkify.addLinks(messageSpannableString, Linkify.ALL) + + val textView = TextView(this) + textView.text = messageSpannableString + textView.movementMethod = LinkMovementMethod.getInstance() + + AlertDialog.Builder(this) + .setTitle(R.string.redirect_modal_title) + .setView(textView) + .setPositiveButton(R.string.ok) { dialog, _ -> + dialog.dismiss() + } + .show() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v24/ic_notification_nys.xml b/app/src/main/res/drawable-anydpi-v24/ic_notification_nys.xml new file mode 100644 index 0000000..6f92d40 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_notification_nys.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_notification_nys.png b/app/src/main/res/drawable-hdpi/ic_notification_nys.png new file mode 100644 index 0000000000000000000000000000000000000000..7df77a01e46dcc9b14a027b774df44037dfd58c7 GIT binary patch literal 373 zcmV-*0gC>KP)MqGPP4bg0rT5R@ zjuYj&**K2lIL^8d(E-=yve=6a0wr41Y7r0h3aAys$y<&Oj!%vT^-?5Pn-Q-q>Q%78 zUd_&^mBP6f_1L2}3pVQ1Fz9cY+!MzO$C=}*A=k1d+HDdvKK9OfE`rT=YWhy8XeqK+ za)cfF&PL2Q;!viZNNY93Mn^DT!ZZ3;LF3D~CU01$2u?hq(~8tJMfu0{rBTaN-T~`o zq46VT>5N`8xs%E3KOL5)*C(NT!Pe*j#RmkF*P=+K;VhJ&j&L=5UQv8NYw~gv5}JO` z9hAR|9y!k;> zTsxQS=k#Ep(S!^(n)4J3SN@cp=Jl}tvv8AI3!j~WJ$mSktTfkSk8Ozft*{V~qP=^# zbrEI^x6g*}et>G|B=s)r)fT*68qMbcPE6n6G@l1distix#gx+g!tb~N@7k@>y;)n# P00000NkvXXu0mjf)RA=_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_nys.png b/app/src/main/res/drawable-xhdpi/ic_notification_nys.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6ba812c29de3b0b2fb37ae169e5e34b801e3c3 GIT binary patch literal 478 zcmV<40U`d0P)FeAJdOqvPy(f3pI@N2lX=-nE{8MTpVE?V^|f-|bz zII4RFs9C9BR^Tu3j~M5K+O_(w2L7TosaH`N; zz2-Fwr1f6m4`Vgr8?=+y61bKc)Vj7=JI!wroO=`HB=h1XY$^IyKIw$OvipGRyFX~B zb;4XmbpG1uF?KrtBDlQ!BWHeyRj8bVxeM?T}Z7lA*IQ?RVQVtEgw^za(gnp3JP z&36FO=IYUyu1F};sO6&7dpXOE1?OuV0L7 zzU>7uKe%RTmVq*Dxn?o0c|~D9xMpdVfihKGvnUr}O@Y>2vy_su{0BcF{_TbOgAD&Q z>v`ziu}t|U=sp)@)eB~JT$7shqzo&r$;t(Kr+_urFjw>BmTzwP>6RB->~FU2iML~} zrpC~oYqA>&8Qk{aPsj%0L>l7*e%s)LVR&0NHP;jYBbT{bzPqKtaDKR!?W=*`clO~O z4Su5RPuNSd-WBI63jE`JF0Y)Y81Pq5OM!D}QQB1mf7dCA%~3C$b8+~5Yc5rTTn~%z zL+aNsPmwHR#rczhf1a81ww>B?uRrj^>K3jXW|`9tH*0WcbgR;yB$PcN$4c|8gz%C6 znBQ*M!@qX$3v3G3!+Kdy>m6go$9ENk-&&5bpo2>o;Y;kCYU*WEfMu96@1%{mre<_} zBYyjH?6{ACKS?omyzNEcZ|O1KdD{-cPoZ;o<~|+75AER7$=NXcY3GzZZ`*O}p1F7i zr%uqGi;uYOCn1EtT1+`M#I{^GgVCRJ5I@8^74IV&&L0|^k$u!$G^2W?--e4Ota%^N zaQUav2L27d?fpHK7QR=53-j`EM?w7Ld=Dx<{z*tPv6gDZ$3F@2cdvV}TAB THREE Home Events + App Link Processed + OK \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1a94d4b..09ef4d1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.32" + ext.kotlin_version = "1.5.20" repositories { mavenCentral() google()