Skip to content

Commit 27e5a91

Browse files
authored
add new kotlin sample app for destinations (#7)
1 parent e63df73 commit 27e5a91

File tree

40 files changed

+1246
-7
lines changed

40 files changed

+1246
-7
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Sample App
2+
This is a sample android app that uses the `analytics-kotlin` library and the new `Plugins` concepts. It is meant to be simplistic, and easy to understand all the while showcasing the power of the analytics-kotlin library
3+
4+
## Plugins
5+
- Android AdvertisingId Plugin
6+
Using the `play-services-ads` library this plugin adds the `advertisingId` to all payloads (under the `context` key) going through the analytics timeline
7+
8+
- Android Record Screen Plugin
9+
Using the application lifecycle, this plugin automatically sends `Screen` events through the analytics timeline, on Activity start
10+
11+
- Consent Tracking
12+
Presents user with a dialog to consent to tracking. If consent is given, any queued events will be sent out to the analytics timeline. If consent is not given, all queued events and future events will be dropped
13+
** Note: You will have to switch to the `ConsentActivity` inside of AndroidManifest.xml to view this feature **
14+
15+
- Webhook Plugin
16+
A destination plugin that allows you to send the event from the analytics timeline to a webhook of your choice. Ideal for debugging payloads in an internal network.
17+
18+
## Tracking Deep Links
19+
The sample app is configured to open links with the schema and hostname `https://segment-sample.com`
20+
21+
Here is how you can do it via adb
22+
```bash
23+
adb shell am start -W -a android.intent.action.VIEW -d "https://segment-sample.com?utm_source=cli\&utm_click=2" com.segment.analytics.next
24+
```
25+
26+
## FCM
27+
This project is setup to track push notification received and opened events. This code is strictly optional and must be customized as per your needs. The code here is only for demonstration purposes
28+
### Setup
29+
- Add your FCM project's `google-services.json` to this folder
30+
- Modify `MyFirebaseService.kt` to customize the notification displayed to the user.
31+
- Here is how to send a push notification using cURL [this uses the legacy api](https://firebase.google.com/docs/cloud-messaging/send-message#send-messages-using-the-legacy-app-server-protocols)
32+
```bash
33+
curl --request POST \
34+
--url https://fcm.googleapis.com/fcm/send \
35+
--header 'Authorization: key=<SERVER_KEY>' \
36+
--header 'Content-Type: application/json' \
37+
--data '{
38+
"data": {
39+
"title": "Hello World"
40+
"content": "You have mail",
41+
},
42+
"to": "<FCM_TOKEN_FOR_DEVICE>"
43+
}'
44+
```
45+
46+
### How it works
47+
- We have 2 core changes
48+
- MyFirebaseService.kt
49+
The core component to handle FCM push messages. This is responsible for handling the incoming message and assigning the intents for the notification. It is also responsible for firing the `Push Notification Received` event.
50+
- PushNotificationTracking.kt
51+
The analytics plugin responsible for firing the "Push Notification Tapped" event. This is a lifecycle plugin that will be invoked for any Activity onCreate.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
plugins {
2+
id 'com.android.application'
3+
id 'kotlin-android'
4+
id 'org.jetbrains.kotlin.plugin.serialization'
5+
}
6+
7+
android {
8+
compileSdkVersion 30
9+
buildToolsVersion "30.0.2"
10+
11+
defaultConfig {
12+
multiDexEnabled true
13+
applicationId "com.segment.analytics.destinations"
14+
minSdkVersion 16
15+
targetSdkVersion 30
16+
versionCode 3
17+
versionName "2.0"
18+
19+
buildConfigField "String", "SEGMENT_WRITE_KEY", "\"${writeKey}\""
20+
21+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22+
}
23+
24+
buildTypes {
25+
release {
26+
minifyEnabled false
27+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28+
}
29+
}
30+
compileOptions {
31+
sourceCompatibility JavaVersion.VERSION_1_8
32+
targetCompatibility JavaVersion.VERSION_1_8
33+
}
34+
kotlinOptions {
35+
jvmTarget = '1.8'
36+
}
37+
}
38+
39+
dependencies {
40+
implementation project(':analytics-kotlin')
41+
42+
implementation 'androidx.multidex:multidex:2.0.1'
43+
44+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
45+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
46+
47+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
48+
implementation 'androidx.core:core-ktx:1.3.2'
49+
implementation 'androidx.appcompat:appcompat:1.2.0'
50+
implementation 'com.google.android.material:material:1.3.0'
51+
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
52+
53+
implementation 'androidx.lifecycle:lifecycle-process:2.3.1'
54+
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
55+
56+
testImplementation 'junit:junit:4.+'
57+
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
58+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
59+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.segment.analytics.next
2+
3+
import androidx.test.platform.app.InstrumentationRegistry
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
9+
import org.junit.Assert.*
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* See [testing documentation](http://d.android.com/tools/testing).
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
class ExampleInstrumentedTest {
18+
@Test
19+
fun useAppContext() {
20+
// Context of the app under test.
21+
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22+
assertEquals("com.segment.analytics.next", appContext.packageName)
23+
}
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.segment.analytics.destinations">
4+
5+
<!-- Required for internet. -->
6+
<uses-permission android:name="android.permission.INTERNET" />
7+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
<uses-permission android:name="android.permission.WAKE_LOCK" />
9+
10+
<!-- Allow you to see which activity was active when a crash occurs. -->
11+
<uses-permission android:name="android.permission.GET_TASKS" />
12+
13+
<!-- Allows location to be tracked. -->
14+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
15+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
16+
17+
<application
18+
android:name=".MainApplication"
19+
android:allowBackup="true"
20+
android:icon="@mipmap/ic_launcher"
21+
android:label="@string/app_name"
22+
android:roundIcon="@mipmap/ic_launcher_round"
23+
android:supportsRtl="true"
24+
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
25+
<meta-data
26+
android:name="com.google.android.gms.ads.APPLICATION_ID"
27+
android:value="ca-app-pub-3940256099942544~3347511713" />
28+
<activity android:name=".MainActivity">
29+
<intent-filter>
30+
<action android:name="android.intent.action.MAIN" />
31+
32+
<category android:name="android.intent.category.LAUNCHER" />
33+
</intent-filter>
34+
</activity>
35+
</application>
36+
37+
</manifest>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.segment.analytics.destinations
2+
3+
import android.os.Bundle
4+
import android.text.Html
5+
import android.text.method.ScrollingMovementMethod
6+
import android.view.LayoutInflater
7+
import android.view.View
8+
import android.view.ViewGroup
9+
import android.widget.Button
10+
import android.widget.EditText
11+
import android.widget.LinearLayout
12+
import android.widget.TextView
13+
import androidx.fragment.app.Fragment
14+
import com.segment.analytics.*
15+
import com.segment.analytics.platform.Plugin
16+
import kotlinx.serialization.encodeToString
17+
import kotlinx.serialization.json.Json
18+
19+
/**
20+
* A UI fragment allowing users to send events through the analytics timeline
21+
* It leverages the Plugin concept to display in-flight events
22+
*/
23+
class EventFragment(val type: EventType, val analytics: Analytics) : Fragment() {
24+
25+
override fun onCreateView(
26+
inflater: LayoutInflater,
27+
container: ViewGroup?,
28+
savedInstanceState: Bundle?
29+
): View {
30+
val props = fetch(type)
31+
// Inflate the layout for this fragment
32+
val view = inflater.inflate(props.second, container, false)
33+
34+
val traitRoot = view.findViewById<LinearLayout>(R.id.props)
35+
// add the first one
36+
addPropertyLayout(traitRoot)
37+
view.findViewById<Button>(R.id.add).setOnClickListener {
38+
addPropertyLayout(traitRoot)
39+
}
40+
41+
view.findViewById<Button>(R.id.sendEvent).setOnClickListener {
42+
val input = view.findViewById<EditText>(R.id.input).text.toString().let {
43+
if (it.isNotEmpty()) {
44+
it
45+
} else {
46+
"Placeholder"
47+
}
48+
}
49+
50+
51+
val properties = getUserProps(traitRoot)
52+
when (type) {
53+
EventType.Track -> sendTrack(eventName = input, props = properties)
54+
EventType.Identify -> sendIdentify(userId = input, traits = properties)
55+
EventType.Screen -> sendScreen(screenName = input, props = properties)
56+
EventType.Group -> sendGroup(groupId = input, traits = properties)
57+
}
58+
}
59+
60+
val codeView = view.findViewById<TextView>(R.id.code_view)
61+
codeView.movementMethod = ScrollingMovementMethod()
62+
63+
analytics.add(object : Plugin {
64+
override val type: Plugin.Type = Plugin.Type.After
65+
override val name: String = "TempResult-$type"
66+
override lateinit var analytics: Analytics
67+
override fun execute(event: BaseEvent): BaseEvent? {
68+
val eventStr = when (event.type) {
69+
EventType.Track -> eventStr(event as TrackEvent)
70+
EventType.Screen -> eventStr(event as ScreenEvent)
71+
EventType.Alias -> eventStr(event as AliasEvent)
72+
EventType.Identify -> eventStr(event as IdentifyEvent)
73+
EventType.Group -> eventStr(event as GroupEvent)
74+
}
75+
val codeString = colorFormat(eventStr)
76+
activity?.runOnUiThread {
77+
codeView.text = Html.fromHtml(codeString)
78+
}
79+
return super.execute(event)
80+
}
81+
})
82+
83+
return view
84+
}
85+
86+
private fun getUserProps(root: LinearLayout): MutableMap<String, String> {
87+
val map = mutableMapOf<String, String>()
88+
for (i in 0 until root.childCount) {
89+
val ll = root.getChildAt(i)
90+
val key = ll.findViewWithTag<EditText>("key").text.toString()
91+
val value = ll.findViewWithTag<EditText>("value").text.toString()
92+
if (key.isNotEmpty()) {
93+
map[key] = value
94+
}
95+
}
96+
return map
97+
}
98+
99+
private fun addPropertyLayout(container: LinearLayout) {
100+
val inflater = LayoutInflater.from(context);
101+
//to get the MainLayout
102+
val layoutXml: Int = when (type) {
103+
EventType.Track -> R.layout.property
104+
EventType.Identify -> R.layout.trait
105+
EventType.Screen -> R.layout.property
106+
EventType.Group -> R.layout.trait
107+
else -> 0
108+
}
109+
val view = inflater.inflate(layoutXml, container, false)
110+
container.addView(view)
111+
}
112+
113+
private inline fun <reified T : BaseEvent> eventStr(event: T) = Json {
114+
prettyPrint = true
115+
encodeDefaults = true
116+
}.encodeToString(event)
117+
118+
private fun colorFormat(text: String): String {
119+
val spacer = fun(match: MatchResult): CharSequence {
120+
return "<br>" + "&nbsp;".repeat(match.value.length - 1)
121+
}
122+
123+
val newString = text
124+
.replace("\".*\":".toRegex(), "<font color=#52BD94>$0</font>")
125+
.replace("\\n\\s*".toRegex(), spacer)
126+
return newString
127+
}
128+
129+
private fun sendGroup(groupId: String, traits: Map<String, String>) {
130+
analytics.group(groupId, traits)
131+
}
132+
133+
private fun sendScreen(screenName: String, props: Map<String, String>) {
134+
analytics.screen(screenName, props)
135+
}
136+
137+
private fun sendIdentify(userId: String, traits: Map<String, String>) {
138+
analytics.identify(userId, traits)
139+
}
140+
141+
private fun sendTrack(eventName: String, props: Map<String, String>) {
142+
analytics.track(eventName, props)
143+
}
144+
145+
private fun fetch(type: EventType): Pair<String, Int> {
146+
return when (type) {
147+
EventType.Track -> "props" to R.layout.fragment_track
148+
EventType.Identify -> "traits" to R.layout.fragment_identify
149+
EventType.Screen -> "props" to R.layout.fragment_screen
150+
EventType.Group -> "traits" to R.layout.fragment_group
151+
else -> "" to 0
152+
}
153+
}
154+
}
155+

0 commit comments

Comments
 (0)