Skip to content

Commit fda8cdc

Browse files
argiadALobastov
andauthored
WebRTC, accumulated updates (#45)
* Implement handling image files * Gradle cleanup and update the dependencies * Code cleanup * Strings extracted to strings.xml * Update Gradle and Kotlin versions to latest * added UX/UI to send image files from user to server side * - Upload files (images) to server - Code cleaning * chat_session_signaling event * New chat_session_form_show event * Poll Request refactoring * gitignore update * Call Activity layout * API and Demo app impl * Gradle 7.5 and Kotlin 1.8 updates * WebRTC library added WebRTC incoming call implementation - DONE * WebRTC library added WebRTC incoming call implementation - DONE * Change log --------- Co-authored-by: Alexander Lobastov <58867146+ALobastov@users.noreply.github.com>
1 parent 2782465 commit fda8cdc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2260
-320
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,4 @@ gradlew.bat
8888

8989
chatdemo/src/.DS_Store
9090
BPContactCenter/.DS_Store
91+
/chatdemo/google-services.json

BPContactCenter/build.gradle

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ plugins {
44
id 'kotlinx-serialization'
55
id 'maven-publish'
66
id 'org.jetbrains.dokka'
7-
87
}
98

109
android {
11-
compileSdkVersion 30
10+
compileSdkVersion 33
1211
buildToolsVersion "30.0.3"
1312

1413
dokkaHtml.configure {
@@ -27,9 +26,7 @@ android {
2726

2827
defaultConfig {
2928
minSdkVersion 22
30-
targetSdkVersion 30
31-
versionCode 1
32-
versionName "1.0"
29+
targetSdkVersion 33
3330

3431
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3532
consumerProguardFiles "consumer-rules.pro"
@@ -48,6 +45,7 @@ android {
4845
kotlinOptions {
4946
jvmTarget = '1.8'
5047
}
48+
namespace 'com.brightpattern.bpcontactcenter'
5149
}
5250

5351
// Because the components are created only during the afterEvaluate phase, you must
@@ -72,14 +70,17 @@ afterEvaluate {
7270
dependencies {
7371

7472
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
75-
implementation 'androidx.core:core-ktx:1.3.2'
76-
implementation 'androidx.appcompat:appcompat:1.2.0'
73+
implementation "androidx.core:core-ktx:$core_ktx"
74+
implementation "androidx.appcompat:appcompat:$app_compat"
7775

78-
testImplementation 'junit:junit:4.13.2'
79-
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
80-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
76+
implementation "com.android.volley:volley:$volley"
77+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_json")
8178

82-
implementation 'com.android.volley:volley:1.1.1'
83-
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
79+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
80+
81+
// ******* TESTS *******
82+
testImplementation 'junit:junit:4.13.2'
83+
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
84+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
8485

85-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'}
86+
}

BPContactCenter/src/main/AndroidManifest.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
package="com.brightpattern.bpcontactcenter">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
43

54
<uses-permission android:name="android.permission.INTERNET" />
65

BPContactCenter/src/main/java/com/brightpattern/bpcontactcenter/ContactCenterCommunicator.kt

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.brightpattern.bpcontactcenter
22

33
import android.os.Build
44
import android.content.Context
5+
import android.graphics.Bitmap
6+
import android.util.Log
57
import com.android.volley.Request
68
import com.android.volley.VolleyError
79
import com.android.volley.toolbox.HurlStack
@@ -17,16 +19,19 @@ import com.brightpattern.bpcontactcenter.model.ContactCenterServiceAvailability
1719
import com.brightpattern.bpcontactcenter.model.ContactCenterVersion
1820
import com.brightpattern.bpcontactcenter.model.http.ChatSessionCaseHistoryDto
1921
import com.brightpattern.bpcontactcenter.model.http.ContactCenterEventsContainerDto
22+
import com.brightpattern.bpcontactcenter.model.ContactCenterUploadedFileInfo
2023
import com.brightpattern.bpcontactcenter.network.NetworkService
2124
import com.brightpattern.bpcontactcenter.network.URLProvider
2225
import com.brightpattern.bpcontactcenter.network.support.HttpHeaderFields
2326
import com.brightpattern.bpcontactcenter.network.support.HttpRequestDefaultParameters
2427
import com.brightpattern.bpcontactcenter.utils.Failure
2528
import com.brightpattern.bpcontactcenter.utils.Result
2629
import com.brightpattern.bpcontactcenter.utils.Success
30+
import com.brightpattern.bpcontactcenter.utils.toBodyEncoded
2731
import kotlinx.serialization.json.Json
2832
import org.json.JSONException
2933
import org.json.JSONObject
34+
import java.io.ByteArrayOutputStream
3035
import java.net.HttpURLConnection
3136
import java.net.URL
3237
import java.util.*
@@ -156,6 +161,12 @@ class ContactCenterCommunicator private constructor(override val baseURL: String
156161
val url = URLProvider.Endpoint.GetChatHistory.generateFullUrl(baseURL, tenantURL, chatID)
157162
networkService.executeSimpleRequest(Request.Method.GET, url, defaultHttpHeaderFields, {
158163
val result = format.decodeFromString(ContactCenterEventsContainerDto.serializer(), it.toString())
164+
// Add URL for file events
165+
result.events.forEach { event ->
166+
(event as? ContactCenterEvent.ChatSessionFile)?.let { message ->
167+
message.url = URLProvider.Endpoint.File.generateFileUrl(baseURL, message.fileUUID)
168+
}
169+
}
159170
completion.invoke(Success(result.events))
160171
}, {
161172
completion.invoke(Failure(parseVolleyError(it)))
@@ -172,6 +183,14 @@ class ContactCenterCommunicator private constructor(override val baseURL: String
172183
val url = URLProvider.Endpoint.GetCaseHistory.generateFullUrl(baseURL, tenantURL, chatID)
173184
networkService.executeSimpleRequest(Request.Method.GET, url, defaultHttpHeaderFields, {
174185
val list = format.decodeFromString(ChatSessionCaseHistoryDto.serializer(), it.toString())
186+
list.sessions.forEach { session ->
187+
// Add URL for file events
188+
session.events.forEach { event ->
189+
(event as? ContactCenterEvent.ChatSessionFile)?.let { message ->
190+
message.url = URLProvider.Endpoint.File.generateFileUrl(baseURL, message.fileUUID)
191+
}
192+
}
193+
}
175194
completion.invoke(Success(list))
176195
}, {
177196
completion.invoke(Failure(parseVolleyError(it)))
@@ -217,22 +236,55 @@ class ContactCenterCommunicator private constructor(override val baseURL: String
217236
}
218237
}
219238

220-
override fun sendChatMessage(chatID: String, content: String, format: ContactCenterCommunicating.ContentFormat, messageID: UUID?, completion: (Result<String, Error>) -> Unit) {
239+
override fun sendChatFile(chatID: String, fileID: String, fileName: String, fileType: String, completion: (Result<List<ContactCenterEvent.ChatSessionFile>, Error>) -> Unit) {
221240
try {
222241
val url = URLProvider.Endpoint.SendEvents.generateFullUrl(baseURL, tenantURL, chatID)
242+
val chatSessionFile = ContactCenterEvent.ChatSessionFile(fileName, fileID, fileType, UUID.randomUUID().toString())
243+
val payload = createSendEventPayload(chatSessionFile)
244+
networkService.executeJsonRequest(Request.Method.POST, url, defaultHttpHeaderFields, payload, {
245+
chatSessionFile.url = URLProvider.Endpoint.File.generateFileUrl(baseURL, fileID)
246+
completion.invoke(Success(listOf(chatSessionFile)))
247+
}, {
248+
completion.invoke(Failure(parseVolleyError(it)))
249+
})
250+
} catch (e: ContactCenterError) {
251+
completion.invoke(Failure(e))
252+
} catch (e: java.lang.Exception) {
253+
completion.invoke(Failure(ContactCenterError.CommonCCError(e.toString())))
254+
}
255+
}
223256

224-
var html = "";
225-
var text = "";
257+
override fun uploadFile(fileName: String, image: Bitmap, completion: (Result<ContactCenterUploadedFileInfo, Error>) -> Unit) {
258+
try {
259+
260+
val boundary = UUID.randomUUID().toString()
261+
262+
val bos = ByteArrayOutputStream()
263+
image.compress(Bitmap.CompressFormat.JPEG, 80, bos)
264+
val bitmapData = bos.toByteArray()
265+
266+
val url = URLProvider.Endpoint.UploadFile.generateFullUrl(baseURL, tenantURL)
226267

227-
if (ContactCenterCommunicating.ContentFormat.TEXT == format) {
228-
text = content
229-
html = content.replace("\n", "<br>")
230-
}
231-
else if (ContactCenterCommunicating.ContentFormat.HTML == format) {
232-
html = content;
233-
}
234268

235-
val payload = createSendEventPayload(ContactCenterEvent.ChatSessionMessage(messageID.toString(), UUID.randomUUID().toString(), html, text))
269+
networkService.executeFileUpload(url, defaultHttpHeaderFields.fileUploadFields(boundary), bitmapData.toBodyEncoded(boundary, fileName),
270+
{
271+
val result = format.decodeFromString(ContactCenterUploadedFileInfo.serializer(), it.toString())
272+
completion.invoke((Success(result)))
273+
}, {
274+
completion.invoke(Failure(parseVolleyError(it)))
275+
})
276+
277+
} catch (e: ContactCenterError) {
278+
completion.invoke(Failure(e))
279+
} catch (e: java.lang.Exception) {
280+
completion.invoke(Failure(ContactCenterError.FileUploadError(e.toString())))
281+
}
282+
}
283+
284+
override fun sendChatMessage(chatID: String, message: String, messageID: UUID?, completion: (Result<String, Error>) -> Unit) {
285+
try {
286+
val url = URLProvider.Endpoint.SendEvents.generateFullUrl(baseURL, tenantURL, chatID)
287+
val payload = createSendEventPayload(ContactCenterEvent.ChatSessionMessage(messageID.toString(), UUID.randomUUID().toString(), message))
236288

237289
networkService.executeJsonRequest(Request.Method.POST, url, defaultHttpHeaderFields, payload, {
238290
completion.invoke(Success(it.toString()))
@@ -389,29 +441,49 @@ class ContactCenterCommunicator private constructor(override val baseURL: String
389441
}
390442
}
391443

444+
override fun sendSignalingData(chatID: String, partyID: String, messageID: Int, data: ContactCenterEvent.SignalingData, completion: (Result<Any, Error>) -> Unit) {
445+
446+
try {
447+
val url = URLProvider.Endpoint.SendEvents.generateFullUrl(baseURL, tenantURL, chatID)
448+
val signalingEvent = ContactCenterEvent.ChatSessionSignaling(data, destination_party_id = partyID, msg_id = "$messageID", party_id = partyID )
449+
val payload = createSendEventPayload(signalingEvent)
450+
451+
networkService.executeJsonRequest(Request.Method.POST, url, defaultHttpHeaderFields, payload, {
452+
completion.invoke(Success(it.toString()))
453+
}, {
454+
completion.invoke(Failure(parseVolleyError(it)))
455+
})
456+
} catch (e: ContactCenterError) {
457+
completion.invoke(Failure(e))
458+
} catch (e: java.lang.Exception) {
459+
completion.invoke(Failure(ContactCenterError.CommonCCError(e.toString())))
460+
}
461+
462+
}
463+
392464
private fun createRequestChatPayload(phoneNumber: String, from: String, parameters: JSONObject): JSONObject {
393465
val payload = JSONObject()
394466
payload.put(FieldName.PHONE_NUMBER, phoneNumber)
395467
payload.put(FieldName.FROM, from)
396468

397-
var parms = parameters
398-
399-
if (parms == null) {
400-
parms = JSONObject()
401-
}
469+
// if (parms == null) {
470+
// parms = JSONObject()
471+
// }
402472

403473
// Add system info
404474
val userPlatform = JSONObject()
405475
userPlatform.put("os", Build.FINGERPRINT)
406476
userPlatform.put("sdk", Build.VERSION.SDK_INT)
407-
userPlatform.put("patch", Build.VERSION.SECURITY_PATCH)
408477
userPlatform.put("manufacturer", Build.MANUFACTURER)
409478
userPlatform.put("model", Build.MODEL)
410479
userPlatform.put("hardware", Build.HARDWARE)
480+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
481+
userPlatform.put("patch", Build.VERSION.SECURITY_PATCH)
482+
}
411483

412-
parms.put("user_platform", userPlatform)
484+
parameters.put("user_platform", userPlatform)
413485

414-
payload.put(FieldName.PARAMETERS, parms)
486+
payload.put(FieldName.PARAMETERS, parameters)
415487

416488
return payload
417489
}

BPContactCenter/src/main/java/com/brightpattern/bpcontactcenter/PollRequest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.brightpattern.bpcontactcenter
22

33
import com.android.volley.Request
4+
import com.brightpattern.bpcontactcenter.entity.ContactCenterEvent
45
import com.brightpattern.bpcontactcenter.interfaces.ContactCenterEventsInterface
56
import com.brightpattern.bpcontactcenter.interfaces.NetworkServiceable
67
import com.brightpattern.bpcontactcenter.model.http.ContactCenterEventsContainerDto
@@ -83,6 +84,13 @@ class PollRequest private constructor(
8384
if (chatID.isNotEmpty() && !isPaused)
8485
networkService.executePollRequest(Request.Method.GET, url, defaultHttpHeaderFields, null, pollInterval, {
8586
val result = format.decodeFromString(ContactCenterEventsContainerDto.serializer(), it.toString())
87+
// Add URL for file events
88+
result.events.forEach { event ->
89+
(event as? ContactCenterEvent.ChatSessionFile)?.let { message ->
90+
message.url = URLProvider.Endpoint.File.generateFileUrl(baseUrl, message.fileUUID)
91+
}
92+
}
93+
8694
callback?.chatSessionEvents(Success(result.events))
8795
runObservation()
8896
}, {

BPContactCenter/src/main/java/com/brightpattern/bpcontactcenter/entity/ContactCenterError.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ sealed class ContactCenterError(text: String) : Error(text) {
3434
data class ChatSessionCrmServerError(val text: String) : ContactCenterError(text)
3535
data class ChatSessionTooManyParameters(val text: String) : ContactCenterError(text)
3636
data class ChatSessionUnspecifiedServerError(val text: String) : ContactCenterError(text)
37+
data class FileUploadError(val text: String) : ContactCenterError(text)
3738

3839
data class CommonCCError(val text: String) : ContactCenterError(text)
3940
}

BPContactCenter/src/main/java/com/brightpattern/bpcontactcenter/entity/ContactCenterEvent.kt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ sealed class ContactCenterEvent {
1717
data class ChatSessionMessage(
1818
@SerialName(FieldName.MSG_ID) val messageID: String = "",
1919
@SerialName(FieldName.PARTY_ID) val partyID: String?,
20-
@SerialName(FieldName.MSG) val message: String? = null,
21-
@SerialName(FieldName.MSG_TEXT) val messageText: String? = null,
20+
@SerialName(FieldName.MSG) val message: String,
2221
val timestamp: Long = System.currentTimeMillis() / 1000) : ContactCenterEvent()
2322

2423
/// Indicates that a message has been delivered
@@ -142,7 +141,8 @@ sealed class ContactCenterEvent {
142141
val fileType: String,
143142
@SerialName(FieldName.PARTY_ID)
144143
val partyID: String,
145-
val timestamp: Long = System.currentTimeMillis() / 1000) : ContactCenterEvent()
144+
var url: String? = null,
145+
val timestamp: Long = System.currentTimeMillis() / 1000) : ContactCenterEvent()
146146

147147
@Serializable
148148
@SerialName("chat_session_form_submit")
@@ -176,6 +176,42 @@ sealed class ContactCenterEvent {
176176
val errorDescription: String = "API received unknown event entry"
177177
) : ContactCenterEvent()
178178

179+
@Serializable
180+
@SerialName("chat_session_signaling")
181+
data class ChatSessionSignaling(
182+
@SerialName("data") val data: SignalingData,
183+
@SerialName(FieldName.PARTY_ID)
184+
val party_id: String? = null,
185+
val msg_id: String = "-1" ,
186+
val destination_party_id: String? = null,
187+
@SerialName(FieldName.TIMESTAMP)
188+
val timestamp: Long = System.currentTimeMillis() / 1000
189+
) :ContactCenterEvent()
190+
191+
@Serializable
192+
data class SignalingData(
193+
val candidate: String? = null,
194+
val sdp: String? = null,
195+
val sdpMLineIndex: String? = null,
196+
val sdpMid: String? = null,
197+
val type: SignalingType? = null
198+
)
199+
200+
@Serializable
201+
@SerialName("chat_session_form_show")
202+
data class ChatSessionFormShow(
203+
@SerialName("channel")
204+
val channel:String,
205+
@SerialName("form_name")
206+
val formName: String,
207+
@SerialName("form_request_id")
208+
val requestID: Int,
209+
@SerialName("form_timeout")
210+
val timeout: Int,
211+
@SerialName(FieldName.TIMESTAMP)
212+
val timestamp: String
213+
) : ContactCenterEvent ()
214+
179215
// TODO: need to implement
180216
companion object {
181217
val jsonSerializer = SerializersModule {
@@ -203,6 +239,8 @@ sealed class ContactCenterEvent {
203239
subclass(ChatSessionCaseSet::class)
204240
subclass(ChatSessionStatus::class)
205241

242+
subclass(ChatSessionSignaling::class)
243+
206244
default { ChatSessionUnknownEvent.serializer() }
207245

208246
}

BPContactCenter/src/main/java/com/brightpattern/bpcontactcenter/entity/FieldName.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ internal class FieldName {
2121
const val LAST_NAME = "last_name"
2222
const val MSG = "msg"
2323
const val MSG_ID = "msg_id"
24-
const val MSG_TEXT = "msg_text"
2524
const val NOTIFICATION_CHAT_ID = "chatID"
2625
const val PARAMETERS = "parameters"
2726
const val PARTY_ID = "party_id"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.brightpattern.bpcontactcenter.entity
2+
3+
enum class SignalingType {
4+
REQUEST_CALL, END_CALL, CALL_REJECTED, ANSWER_CALL, OFFER_CALL, ICE_CANDIDATE
5+
}

0 commit comments

Comments
 (0)