Skip to content
This repository has been archived by the owner on Nov 21, 2021. It is now read-only.

[BREAKING CHANGE][2.1.0] TalkSDK #29

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 2.1.0+0

### [BREAKING CHANGES]

* Android Min API 21 (Talk SDK minimum required)

### [Features]

* TalkSDK

# 2.0.0+0

## Huge update 🎉
Expand Down
10 changes: 5 additions & 5 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'br.com.adriankohls.zendesk2'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.5.21'
ext.kotlin_version = '1.5.30'
repositories {
google()
jcenter()
Expand All @@ -27,14 +27,14 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 30
compileSdkVersion 31

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
minSdkVersion 21
targetSdkVersion 31
}
lintOptions {
disable 'InvalidPackage'
Expand All @@ -47,5 +47,5 @@ dependencies {

implementation group: 'com.zendesk', name: 'chat-providers', version: '3.3.0'
implementation group: 'com.zendesk', name: 'answerbot-providers', version: '3.0.2'

implementation "zendesk.talk:talk-android:1.1.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class Zendesk2Answer(private val plugin: Zendesk2Plugin, private val channel: Me
} else {
print("Plugin Context is NULL!")
}

}

fun deflectionQuery(call: MethodCall) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,32 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import zendesk.chat.*
import zendesk.talk.android.AudioDevice
import zendesk.talk.android.Talk
import zendesk.talk.android.TalkCall

class Zendesk2Plugin : ActivityAware, FlutterPlugin, MethodCallHandler {

private lateinit var channel: MethodChannel

var activity: Activity? = null

val chatStateObservationScope: ObservationScope = ObservationScope()
val accountObservationScope: ObservationScope = ObservationScope()
val settingsObservationScope: ObservationScope = ObservationScope()
val connectionStatusObservationScope: ObservationScope = ObservationScope()

var talk: Talk? = null
var talkCall: TalkCall? = null
var availableAudioDevices: List<Map<String, Any?>>? = null

var streamingChatSDK: Boolean = false
var streamingAnswerSDK: Boolean = false

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
val zendesk2Chat = Zendesk2Chat(this, channel)
val zendesk2Answer = Zendesk2Answer(this, channel)
val zendesk2Talk = Zendesk2Talk(this, channel)


var mResult: Any? = null
Expand Down Expand Up @@ -76,6 +84,27 @@ class Zendesk2Plugin : ActivityAware, FlutterPlugin, MethodCallHandler {
"sendAnswerProviderModel" -> mResult = call.arguments
"sendResolveArticleDeflection" -> mResult = call.arguments
"sendRejectArticleDeflection" -> mResult = call.arguments
// talk sdk method channels
"init_talk" -> {
if (talk == null) {
zendesk2Talk.initialize(call)
} else {
print("Talk SDK is already running!")
}
}
"talk_recording_permission" -> mResult = zendesk2Talk.recordingPermission()
"talk_check_availability" -> zendesk2Talk.checkAvailability(call)
"talk_call" -> zendesk2Talk.call(call)
"talk_disconnect" -> zendesk2Talk.disconnect()
"talk_toggle_mute" -> mResult = zendesk2Talk.toggleMute()
"talk_toggle_output" -> mResult = zendesk2Talk.toggleOutput()
"talk_available_audio_routing_options" -> {
val dictionary = mutableMapOf<String, Any?>()
dictionary["availableAudioRoutingOptions"] = availableAudioDevices
mResult = dictionary
}
"sendTalkAvailability" -> mResult = call.arguments
"sendTalkCall" -> mResult = call.arguments
else -> print("method not implemented")
}

Expand Down
229 changes: 229 additions & 0 deletions android/src/main/kotlin/br/com/adriankohls/zendesk2/Zendesk2Talk.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package br.com.adriankohls.zendesk2

import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import zendesk.core.AnonymousIdentity
import zendesk.core.Identity
import zendesk.core.JwtIdentity
import zendesk.core.Zendesk
import zendesk.talk.android.*
import zendesk.talk.android.compat.CallDataBuilder

class Zendesk2Talk(private val plugin: Zendesk2Plugin, private val channel: MethodChannel) {

fun initialize(call: MethodCall) {
val appId = call.argument<String>("appId")!!
val clientId = call.argument<String>("clientId")!!
val zendeskUrl = call.argument<String>("zendeskUrl")!!

val token = call.argument<String>("token")
val name = call.argument<String>("name")
val email = call.argument<String>("email")

if (plugin.activity != null) {
val zendesk = Zendesk.INSTANCE

zendesk.init(plugin.activity!!, zendeskUrl, appId, clientId)

val identity: Identity =
if (token != null) {
JwtIdentity(token)
} else {
val builder = AnonymousIdentity.Builder()
if (name != null) builder.withNameIdentifier(name)
if (email != null) builder.withEmailIdentifier(email)
builder.build()
}
zendesk.setIdentity(identity)

plugin.talk = Talk.create(zendesk)

} else {
print("Plugin Context is NULL!")
}
}

fun recordingPermission(): Map<String, Any?> {
val permissionGranted = plugin.talk?.arePermissionsGranted() ?: false

val talkPermission = if (permissionGranted) {
"GRANTED"
} else {
"UNKNOWN"
}

val dictionary = mutableMapOf<String, Any?>()
dictionary["talkPermission"] = talkPermission
return dictionary
}

fun checkAvailability(call: MethodCall) {
val digitalLineName = call.argument<String>("digitalLineName")!!

GlobalScope.launch {
var isAgentAvailable = false
var recordingConsent: RecordingConsent? = null
var error: String? = null

when (val lineStatusResult = plugin.talk?.lineStatus(digitalLineName)) {
is LineStatusResult.Success -> {
isAgentAvailable = lineStatusResult.agentAvailable
recordingConsent = lineStatusResult.recordingConsent
}
LineStatusResult.Failure.DigitalLineNotFound -> {
error = "DigitalLineNotFound"
}
LineStatusResult.Failure.Unknown -> {
error = "UNKNOWN"
}
null -> error = "UNKNOWN"
}


val consent = when (recordingConsent) {
RecordingConsent.OPT_IN -> "OPT_IN"
RecordingConsent.OPT_OUT -> "OPT_OUT"
null -> "UNKNOWN"
}
val dictionary = mutableMapOf<String, Any?>()
dictionary["error"] = error
dictionary["isAgentAvailable"] = isAgentAvailable
dictionary["recordingConsent"] = consent

plugin.activity?.runOnUiThread {
channel.invokeMethod("sendTalkAvailability", dictionary)
}
}
}

fun call(call: MethodCall) {
val digitalLineName = call.argument<String>("digitalLineName")!!
val recordingConsentAnswer = call.argument<String>("recordingConsentAnswer")!!

val consent = when (recordingConsentAnswer) {
"OPT_IN" -> RecordingConsentAnswer.OPTED_IN
"OPT_OUT" -> RecordingConsentAnswer.OPTED_OUT
else -> null
}

val callData = CallDataBuilder.create(digitalLineName)
.recordingConsentAnswer(consent)
.build()

if (
ContextCompat.checkSelfPermission(
plugin.activity!!,
Manifest.permission.RECORD_AUDIO) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
plugin.activity!!,
arrayOf(Manifest.permission.RECORD_AUDIO),
0
)

}



GlobalScope.launch {
var error: String? = null

when (val result = plugin.talk?.createCall(callData)) {
is TalkCallResult.Success -> {
plugin.talkCall = result.talkCall

result.availableAudioDevices.collect { its ->
val mAvailableAudioDevices = mutableListOf<Map<String, Any?>>()

its.forEach {
val name = it.audioOutput.name.uppercase()
val type = when (it.audioOutput) {
AudioOutput.SPEAKERS -> "BUILT_IN"
AudioOutput.HEADSET -> "BUILT_IN"
AudioOutput.BLUETOOTH -> "BLUETOOTH"
else -> "UNKNOWN"
}
val audioDict = mutableMapOf<String, Any?>()
audioDict["name"] = name
audioDict["type"] = type
mAvailableAudioDevices.add(audioDict)
}
plugin.availableAudioDevices = mAvailableAudioDevices
}

result.statusChanges.collect {
val callStatus = when (it) {
CallStatus.CALL_CONNECTED -> "CONNECTED"
CallStatus.CALL_DISCONNECTED -> "DISCONNECTED"
CallStatus.CALL_DISCONNECTED_CONNECTION_ERROR -> "FAILED"
CallStatus.CALL_FAILED -> "FAILED"
CallStatus.CALL_RECONNECTING -> "RECONNECTING"
CallStatus.CALL_RECONNECTED -> "RECONNECTED"
else -> "UNKNOWN"
}
print(it)

val dictionary = mutableMapOf<String, Any?>()
dictionary["error"] = null
dictionary["callStatus"] = callStatus

plugin.activity?.runOnUiThread {
channel.invokeMethod("sendTalkCall", dictionary)
}
}

}
TalkCallResult.Failure.DigitalLineNotFound -> error = "DigitalLineNotFound"
else -> error = "UNKNOWN"
}

if(error != null) {
val dictionary = mutableMapOf<String, Any?>()
dictionary["error"] = error
dictionary["callStatus"] = "UNKNOWN"
plugin.activity?.runOnUiThread {
channel.invokeMethod("sendTalkCall", dictionary)
}
}
}
}

fun toggleMute(): Map<String, Any?> {
var isMuted = plugin.talkCall?.isMuted() ?: false
isMuted = !isMuted
plugin.talkCall?.mute(isMuted)

val dictionary = mutableMapOf<String, Any?>()
dictionary["isMuted"] = isMuted
return dictionary
}

fun toggleOutput(): Map<String, Any?> {
var isSpeaker = plugin.talkCall?.getAudioOutput() == AudioOutput.SPEAKERS

val audioOutput = if (isSpeaker) AudioOutput.BLUETOOTH else AudioOutput.SPEAKERS
plugin.talkCall?.setAudioOutput(audioOutput)

isSpeaker = !isSpeaker

val dictionary = mutableMapOf<String, Any?>()
dictionary["isSpeaker"] = isSpeaker
return dictionary
}

fun disconnect() {
plugin.talkCall?.disconnect()
plugin.talkCall = null
}


}
8 changes: 4 additions & 4 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 30
compileSdkVersion 31

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
Expand All @@ -39,8 +39,8 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "br.com.adriankohls.zendesk2_example"
minSdkVersion 16
targetSdkVersion 30
minSdkVersion 21
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand All @@ -59,5 +59,5 @@ flutter {
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30"
}
5 changes: 5 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name="io.flutter.app.FlutterApplication"
android:label="zendesk2_example"

android:icon="@mipmap/ic_launcher">
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:launchMode="singleTop"
android:exported="false"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
Expand Down
Loading