במאמר הזה מוסבר איך לשלב את Headless Mobile SDK באפליקציה ל-Android ולהתאים אותו אישית. הוא כולל את הקול, שיתוף המסך, שיחות מתוזמנות ואימייל.
שיחת טלפון
Headless Mobile SDK ל-Android מספק תמיכה בשיחות קוליות באמצעות המודולים CCAICall ו-CCAICallRed. האפליקציה שלכם היא הבעלים של ממשק המשתמש המותאם אישית לשיחות, בעוד שערכת ה-SDK מטפלת ביצירת שיחות, בטיפול בשיחות נכנסות, בשילוב ספקים ובמצב מחזור החיים של השיחות.
מה שערכת ה-SDK מספקת: ממשקי API של שירותי שיחות להפעלת שיחות מיידיות, להפעלת דואר קולי, לטיפול בשיחות נכנסות ולמעקב אחרי מצב השיחה.
מה שאתם בונים: חוויית השיחה החזותית, כולל נקודת הכניסה לפני השיחה, ההנחיה למתן הסכמה להקלטה, מצבי הצלצול או ההמתנה, אמצעי הבקרה במהלך השיחה, ממשק המשתמש של שירות הרקע וכל הניווט אחרי השיחה.
הפעלת מודול השיחות
מאתחלים את מודול השיחות אחרי שמאתחלים את ה-SDK המרכזי:
import com.ccaiplatform.ccaikit.CCAI
import com.ccaiplatform.ccaicall.CallOptions
import com.ccaiplatform.ccaicall.initializeCall
CCAI.initialize(context = applicationContext, options = initOptions)
CCAI.initializeCall(
context = applicationContext,
options = CallOptions()
)
בדיקת היכולות של VoiceCallChannel
לפני שמציעים שיחה מיידית, בודקים את VoiceCallChannel בתור כדי להחליט אילו נקודות כניסה יוצגו (שיחה מיידית, מעבר אוטומטי לתא קולי, הפניית שיחה ברמת הערוץ) והאם נדרשת הסכמה להקלטה.
שדות מפתח VoiceCallChannel
instantEnabled:Boolean– האם מותר לבצע שיחות קוליות בזמן אמת (מיידיות) בתור הזה. התייחסו אלfalseכאל 'אל תציעו לחצן של שיחה מיידית'. שונה מ-voiceCall != null, שרק מציין אם יש תור עם ערוץ קולי.
preSessionSmartAction:Boolean– האם נדרשות פעולות חכמות לפני תחילת הסשן בערוץ הספציפי הזה לפני שמתחילים שיחה. במאמר Headless Mobile SDK for Android: Smart Actions, Attachments, and Deflection מוסבר על הנתיב המשלים ברמת התור, בסעיף Pre-session smart actions.
scheduleEnabled:Boolean– האם יש תמיכה בשיחות מתוזמנות. מידע נוסף מופיע בקטע שיחות מתוזמנות.
recordingOption:RecordingOption?– מודל ההסכמה להקלטת שיחות. איך מתעדים הסכמה שונה מ-RecordingPermission.NOT_ASKED, שמשמש רק עם שיחות מתוזמנות.
voicemailReason:String?– מאוכלס כשהערוץ מוגדר להעברה להודעה קולית.
phoneNumber:String?– מספר חלופי ב-PSTN שהוגדר להעברה או לתא קולי.
scheduleDeflectionType:String?/deflected:Boolean?/deflectedReason:String?— מטא-נתונים של הפניית שיחות ברמת הערוץ. כשמתקשרים אלdeflected == true, מוצגת ההודעהdeflectedReasonשהאדמין הגדיר ומוצעת האפשרות החלופית שהוגדרה (למשל, מספר טלפון לחיוג או נקודת כניסה לשיחה מתוזמנת) במקום לנסות להתקשר באופן מיידי. אפשר לעיין בקטע הפניית שיחות בזמן ההמתנה ב-Headless Mobile SDK ל-Android: פעולות חכמות, קבצים מצורפים והפניית שיחות לטיפול בהפניית שיחות ברמת התור.
fun voiceEntryPoints(menu: QueueMenu) {
val voice = menu.channels.voiceCall
if (voice == null) {
callNowButton.isVisible = false
voicemailButton.isVisible = false
return
}
// Instant call - only when explicitly enabled and not deflected
callNowButton.isVisible = voice.instantEnabled && voice.deflected != true
// Voicemail fallback
val number = voice.phoneNumber
if (voice.voicemailReason != null && number != null) {
voicemailButton.isVisible = true
voicemailButton.text = "Leave a voicemail"
voicemailButton.setOnClickListener {
dialVoicemail(number, reason = voice.voicemailReason)
}
}
// Channel-level deflection
if (voice.deflected == true) {
showChannelDeflection(reason = voice.deflectedReason)
}
}
הסכמה להקלטה
מה ה-SDK מספק: שדה recordingOption: RecordingOption? ב-VoiceCallChannel שמתאר את מודל ההסכמה להקלטת שיחות שהאדמינים הגדירו בפורטל.
מה יוצרים: את בקשת ההסכמה (כשמשתמשים ב-ASK_USER) ואת לוגיקת ההסתעפות שמכבדת את הבחירה של האדמין.
enum class RecordingOption {
ALWAYS, // Calls are always recorded - surface a disclosure before connecting
NEVER, // Calls are never recorded - no disclosure needed
ASK_USER // Prompt the user for consent before the call connects
}
fun handleRecordingConsent(voice: VoiceCallChannel, onResult: (Boolean) -> Unit) {
when (voice.recordingOption) {
RecordingOption.ALWAYS -> {
showRecordingDisclosure("This call will be recorded.")
onResult(true)
}
RecordingOption.NEVER, null -> onResult(true)
RecordingOption.ASK_USER -> presentConsentPrompt { granted ->
onResult(granted)
}
}
}
התחלת שיחה מיידית
לפני שמתחילים שיחה מיידית:
בודקים את
VoiceCallChannelשל התור שנבחר.בודקים שהאפשרות לשיחות מיידיות מופעלת ושהשיחות לא מועברות.
הטיפול בהסכמה להקלטה מתבצע בהתאם ל
recordingOptionשל התור.שולחים בקשה להרשאת גישה למיקרופון.
התקשרות אל
startInstantCall
suspend fun initiateInstantCall(queueMenu: QueueMenu) {
val voice = queueMenu.channels.voiceCall ?: return
if (!voice.instantEnabled || voice.deflected == true) {
return
}
val recordingPermission = resolveRecordingPermission(voice)
requestMicrophonePermissionIfNeeded()
try {
CCAI.callService?.startInstantCall(
menuId = queueMenu.id,
recordingPermission = recordingPermission
)
} catch (e: Exception) {
showCallStartError(e)
}
}
התבוננות במצב השיחה
איסוף אירועים של שירותי שיחות לפני התחלת שיחה או מענה לשיחה:
val service = CCAI.callService ?: return
lifecycleScope.launch {
service.stateChanged.collect { state ->
updateCallConnectionState(state)
}
}
lifecycleScope.launch {
service.callReceived.collect { call ->
updateCallDetails(call)
}
}
lifecycleScope.launch {
service.incomingCallEvent.collect { event ->
handleIncomingCallEvent(event)
}
}
lifecycleScope.launch {
service.waitTimeUpdated.collect { waitTime ->
updateEstimatedWaitTime(waitTime)
}
}
lifecycleScope.launch {
service.participantUpdated.collect { participant ->
updateParticipantInfo(participant)
}
}
lifecycleScope.launch {
service.interruptionDetected.collect { interruption ->
// A PSTN interruption (for example, an incoming cellular call)
// has paused the active CCAI call. Surface a "call interrupted"
// banner and offer a Resume action after the interruption ends.
showInterruptionBanner(interruption)
}
}
lifecycleScope.launch {
service.deflectionOffered.collect { deflection ->
// The platform offered a wait-time deflection (for example, a
// scheduled callback or PSTN fallback). Render the prompt and
// honor the user's choice.
presentWaitTimeDeflection(deflection)
}
}
המצב של הספק לעומת סטטוס השרת
stateChanged ו-callReceived.status הם אותות עצמאיים – צריך לקרוא אותם כעמודות ולא כשורות. ב-stateChanged של הספק מדווח על לחיצת היד של ה-VoIP המקומי, וב-callReceived.status בצד השרת מדווח על מחזור החיים של השיחה בפלטפורמה. הפעלת המתג 'מחובר' והתחלת הטיימר של השיחה מ-callReceived.status – ולא מהגעה של stateChanged למצב מחובר.
| אות | מה נכלל בדיווח | מה אפשר להסיק ממנו |
|---|---|---|
stateChanged (ספק) |
העברת נתונים (handshake) ב-VoIP מקומי – Connecting, Connected (local), Disconnected, Error(...). |
אופציונלי: אנימציית טעינה לפני שהסטטוס של השרת מגיע, לפני שהשגיאה מוצגת ולפני ניקוי הנתונים בסיום השיחה. |
callReceived.status (שרת) |
מחזור החיים בצד השרת – WAITING, CONNECTING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED. |
החלפת שיחה, הפעלת שעון העצר של השיחה, ממשק משתמש להקצאת סוכן, שכבות-על של העברה או העברה לטיפול ברמה גבוהה יותר ורצף הפעולות לסיום השיחה. |
שיחות נכנסות
שיחות קוליות נכנסות מועברות למכשיר באמצעות FCM. יכול להיות שתקבלו שיחות נכנסות כשהאפליקציה פועלת ברקע או כשהיא סגורה לגמרי, ולכן השילוב צריך להיות מוכן לקבל שיחות מכל מצב של התהליך.
מה ה-SDK מספק: IncomingCallEvent Flow ב-callService שפולט את מחזור החיים של שיחה נכנסת: Arrived, Accepted, Rejected. שירות השיחות גם חושף את acceptIncomingCall() ואת rejectIncomingCall() כדי לפעול על המבצע.
מה שאתם יוצרים: ממשק המשתמש של השיחות הנכנסות (מסך שיחה נכנסת במסך מלא, תצוגה במסך הנעילה או באנר בתוך האפליקציה) והמטפלים שמקבלים או דוחים את ההצעה.
דוגמה להטמעה
אוספים את incomingCallEvent ומציגים או מסתירים את ממשק המשתמש בכל אירוע במחזור החיים, ואז מאשרים או דוחים מטווח ארוך.
צריך להגדיר את applicationScope פעם אחת בכיתה Application כדי שקורוטינות של אישור או דחייה ימשיכו לפעול גם אם יש Activity פירוק קצר במהלך הלחיצת יד.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
// Define this in your Application class (or another long-lived singleton).
// Expose it however your app prefers as top-level property, dependency
// injection, or a static field on the Application subclass.
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
lifecycleScope.launch {
CCAI.callService?.incomingCallEvent?.collect { event ->
when (event) {
is IncomingCallEvent.Arrived -> showIncomingCallUi(event.call)
is IncomingCallEvent.Accepted -> navigateToCallScreen(event.call)
is IncomingCallEvent.Rejected -> dismissIncomingCallUi()
}
}
}
// User taps "Accept" in your custom UI.
fun onAcceptTapped() {
// Important: accept/reject helpers suspend internally. Launch them on
// `applicationScope` (defined above) so the call survives if the
// ringing Activity is dismissed during the handshake.
applicationScope.launch {
CCAI.callService?.acceptIncomingCall()
}
}
fun onRejectTapped() {
applicationScope.launch {
CCAI.callService?.rejectIncomingCall()
}
}
גישור בין FCM לממשק המשתמש של שיחות נכנסות
שיחות נכנסות שמועברות דרך FCM מגיעות בשרשור ברקע, לעיתים קרובות לפני שקיים Activity כלשהו. משתמשים בערך זמן קצוב לתפוגה קצר כדי לגשר בין מטען ייעודי (payload) של FCM לבין הפליטה הראשונה של IncomingCallEvent.Arrived, ואז מפעילים את ממשק המשתמש.
דוגמה להטמעה
משהים את הניווט ב-IncomingCallEvent.Arrived הבא עם withTimeoutOrNull, ואז מעבירים את הניווט אל הנווט.
suspend fun handleIncomingCallPush() {
val arrived = withTimeoutOrNull(60_000L) {
CCAI.callService?.incomingCallEvent
?.filterIsInstance<IncomingCallEvent.Arrived>()
?.first()
} ?: return
// `myNavigator` is the `IncomingCallUiNavigator` implementation you wire
// up during app initialization (see "Alternative: full-screen ringing
// Activity" below). Hold the reference somewhere long-lived (for
// example, on your `Application` subclass) so it can be reused here.
myNavigator.showRingingUi(applicationContext, arrived.call.id)
}
שילוב של מסגרת טלקום (מומלץ)
כדי ליהנות מחוויית שיחה נכנסת מובנית ב-Android – תוכנת צלצול במסך מלא, הצגה במסך הנעילה ושילוב עם יומן השיחות של המערכת – צריך לרשום PhoneAccount ולאפשר למסגרת Telecom לנהל את חוויית המשתמש של הצלצול.
מה ה-SDK מספק: TelecomCallService עוזר שרושם PhoneAccount לאפליקציה (registerPhoneAccount(context)), TelecomConnectionService כדי לגשר בין שיחות CCAI נכנסות לבין Android Telecom, וממשק TelecomConnectionCallback לקריאות חוזרות של מצב החיבור.
מה יוצרים: רישום חד-פעמי של PhoneAccount במהלך אתחול האפליקציה והצהרת מניפסט לשירות החיבור.
דוגמה להטמעה
רושמים את חשבון הטלפון בהפעלת האפליקציה, ואז מצהירים על שירות החיבור וההרשאה במניפסט.
// In Application.onCreate(), after CCAI.initializeCall(...)
TelecomCallService.registerPhoneAccount(context = this)
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<service
android:name="com.ccaiplatform.android.call.TelecomConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
אפשרות חלופית: פעילות של שיחה נכנסת במסך מלא
אם אתם מעדיפים לעבד את מסך השיחה הנכנסת בעצמכם במקום להשתמש ב-Telecom framework, ה-SDK חושף את IncomingCallService יחד עם הממשק IncomingCallUiNavigator. מטמיעים את הממשק, ואז מעבירים את ההטמעה אל IncomingCallService במהלך אתחול האפליקציה.
דוגמה להטמעה
מטמיעים את IncomingCallUiNavigator ומעבירים אותו אל IncomingCallService פעם אחת במהלך Application.onCreate().
// 1. Implement the navigator. Override the three interface methods to
// declare the ringing Activity intent and to show/hide your UI as the
// call lifecycle changes.
class MyNavigator : IncomingCallUiNavigator {
override fun ringingActivityIntent(
context: Context,
callId: Long,
callerName: String?
): Intent = Intent(context, RingingActivity::class.java).apply {
putExtra("callId", callId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
override fun showRingingUi(context: Context, callId: Long) {
// Optional: surface an in-app banner or push your ringing UI.
}
override fun hideRingingUi(context: Context, callId: Long) {
// Optional: dismiss whatever you surfaced in showRingingUi.
}
}
// 2. Wire it up during app initialization (typically in Application.onCreate()).
val incomingCallService = IncomingCallService(
applicationContext,
MyNavigator()
)
if (!CCAI.canShowIncomingCallFullScreen(context)) {
CCAI.openFullScreenIntentSettings(context)
}
שירות שפועל בחזית לצלצול פעיל
בזמן שמתקבלת שיחה, מערכת Android עשויה להפסיק תהליכים שפועלים ברקע. ה-SDK כולל IncomingRingingService ששומר על רצף הפעולות של השיחה כשהיא מתקבלת כשירות שפועל בחזית. צריך להצהיר על השירות במניפסט, וה-SDK יתחיל ויפסיק אותו באופן אוטומטי כשיגיעו שיחות נכנסות.
<service
android:name="com.ccaiplatform.android.call.IncomingRingingService"
android:foregroundServiceType="phoneCall"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
סטטוס השיחה והמצבים בצד השרת
מה ה-SDK מספק: ספירת enum מונחית-שרת CallStatus בכל מטען ייעודי (payload) של Call
שמתארת את המיקום של הקריאה במחזור החיים שלה (לדוגמה,
CONNECTING, WAITING, CONNECTED, ON_HOLD, INTERRUPTED, ENDED,
FAILED).
מה יוצרים: המיפוי מ-CallStatus למצבים שמוצגים ללקוחות במסך השיחה.
דוגמה להטמעה
when-branch on call.status from each
callReceived emission and render the matching UI state:
fun updateCallDetails(call: Call) {
when (call.status) {
CallStatus.CONNECTING -> renderConnectingState()
CallStatus.WAITING -> renderWaitingInQueueState(call.estimatedWait)
CallStatus.CONNECTED -> renderConnectedState(call.agent)
CallStatus.ON_HOLD -> renderOnHoldState()
CallStatus.INTERRUPTED -> renderInterruptedState()
CallStatus.ENDED, CallStatus.FAILED -> renderEndedState(call.endReason)
}
}
אמצעי בקרה במהלך השיחה (השתקה, רמקול, החזקה)
מה ה-SDK מספק: מאפיינים לקריאה בלבד ב-callService – isMuted, isSpeakerEnabled, isOnHold – לצד הפעולות להחלפת כל מצב (setMuted, setSpeakerEnabled, setOnHold).
מה יוצרים: לחצני ההשתקה, הרמקול וההמתנה בממשק המשתמש בשיחה, שמחוברים לפעולות של ה-SDK ומשקפים את המצב הנוכחי.
דוגמה להטמעה
קוראים את המצב הנוכחי מ-callService כדי לאתחל את הלחצנים, ואז מעבירים בין המצבים ומעדכנים בלחיצה.
muteButton.isSelected = CCAI.callService?.isMuted == true
speakerButton.isSelected = CCAI.callService?.isSpeakerEnabled == true
holdButton.isSelected = CCAI.callService?.isOnHold == true
fun onMuteTapped() {
val next = CCAI.callService?.isMuted != true
CCAI.callService?.setMuted(next)
muteButton.isSelected = next
}
fun onSpeakerTapped() {
val next = CCAI.callService?.isSpeakerEnabled != true
CCAI.callService?.setSpeakerEnabled(next)
speakerButton.isSelected = next
}
fun onHoldTapped() {
val next = CCAI.callService?.isOnHold != true
CCAI.callService?.setOnHold(next)
holdButton.isSelected = next
}
המשך שיחה פעילה אחרי הפרעה
כששיחה פעילה ב-CCAI מושהית בגלל הפרעה ברשת PSTN (למשל שיחה סלולרית נכנסת), ה-SDK שומר על הפעלת הסשן ושולח את interruptionDetected.
אחרי שההפרעה מסתיימת, האפליקציה יכולה להמשיך את השיחה.
מה ה-SDK מספק: getLastCallInProgress() כדי לחפש שיחה בתהליך (כולל אחרי הפעלה מחדש של האפליקציה) וresumeCall(call) כדי להחזיר אותה לחזית.
מה אתם יוצרים: מזמינוּת המשך השיחה בממשק המשתמש (בדרך כלל באנר או המשך אוטומטי אחרי שההפרעה מסתיימת) ואת הניווט חזרה למסך השיחה.
דוגמה להטמעה
מחפשים את השיחה הפעילה באמצעות getLastCallInProgress, חוזרים אליה וחוזרים למסך השיחה.
lifecycleScope.launch {
val call = CCAI.callService?.getLastCallInProgress() ?: return@launch
try {
CCAI.callService?.resumeCall(call)
navigateToCallScreen(call)
} catch (e: Exception) {
showCallResumeError(e)
}
}
העברה לטיפול ברמה גבוהה יותר מנציג וירטואלי
מה ה-SDK מספק: canEscalate(allowSkipVirtualAgent: Boolean) היא שיטה ב-CallResponse (המודל שמופק על ידי callService.callReceived), ולא ב-callService עצמו. הפונקציה מחזירה ערך בוליאני שמציין אם השיחה הנוכחית עומדת בדרישות להעברה לנציג אנושי, על סמך הסטטוס הנוכחי של הסוכן הווירטואלי בשיחה.
מה יוצרים: שומרים את פליטת callReceived האחרונה כ'שיחה הנוכחית', ואז מקשרים את הנראות של הלחצן 'לדבר עם נציג' ל-currentCall?.canEscalate(...). הארגומנט allowSkipVirtualAgent הוא
ערך של הגדרת תפריט שמאחזרים בנפרד מתשובת השיחה עצמה
– אל תקודדו את true באופן קשיח בסביבת הייצור.
דוגמה להטמעה
שומרים את הגרסה האחרונה של Call בנכס, ואז מפעילים את canEscalate. צריך לפתור את הבעיה allowSkipVirtualAgent בהגדרות התפריט.
// Held from the most recent `callReceived` emission
private var currentCall: Call? = null
// `allowSkipVirtualAgent` is fetched separately from menu configuration
// it is not derived from the CallResponse.
val allowSkip = menuConfiguration.allowSkipVirtualAgent
val allowed = currentCall?.canEscalate(allowSkipVirtualAgent = allowSkip) == true
talkToAgentButton.isVisible = allowed
הודעה קולית
מה ה-SDK מספק: startVoicemail(VoicemailRequest) ב-callService,
בנוסף ל-enum VoicemailReason שמתאר למה הפלטפורמה הציעה דואר קולי
(לדוגמה, מעבר לגיבוי בשעות לא פעילות, מעבר לגיבוי בקיבולת יתר או הפניה זמנית).
מה שאתם יוצרים: נקודת הכניסה לתא הקולי בממשק המשתמש (בדרך כלל מוצגת כשהשדה VoiceCallChannel.voicemailReason מאוכלס, או כשמחזור החיים של השיחה מסתיים בהשארת הודעה בתא הקולי) וממשק המשתמש של ההקלטה עצמו.
דוגמה להטמעה
הצגת לחצן הדואר הקולי כשמוגדר voicemailReason
ואז התקשרות אל startVoicemail עם VoicemailRequest.
enum class VoicemailReason {
AFTER_HOUR_DEFLECTION,
OVER_CAPACITY_DEFLECTION,
TEMPORARY_REDIRECTION
}
suspend fun offerVoicemail(menu: QueueMenu) {
val voice = menu.channels.voiceCall ?: return
if (voice.voicemailReason == null) return
try {
CCAI.callService?.startVoicemail(
VoicemailRequest(menuId = menu.id)
)
} catch (e: Exception) {
showVoicemailError(e)
}
}
הפניית שיחות בזמן המתנה
כשהתור פתוח אבל זמן ההמתנה חורג מהסף שהאדמין הגדיר, הפלטפורמה יכולה להציע דרך חלופית – בדרך כלל שיחה חוזרת מתוזמנת או העברה לרשת הטלפוניה הציבורית (PSTN).
מה ה-SDK מספק: מטען ייעודי (payload) של CallDeflection שמתאר את החלופה המוצעת, שאפשר לאחזר באמצעות getCallDeflection(callId) ב-callService. בנוסף, הפלטפורמה משדרת את אותו מטען ייעודי (payload) דרך deflectionOffered Flow כשניתן להפנות את השיחה לנציג בזמן ההמתנה. דגל נפרד CompanyResponse.preventDirectPstnCall מציין אם חיוג ישיר לרשת PSTN צריך להיות מוסתר בממשק המשתמש של הגיבוי.
מה יוצרים: את ההודעה להפניית שיחות (לדוגמה, 'זמן ההמתנה המשוער הוא 12 דקות – רוצה לקבוע שיחה חוזרת במקום זאת?') ואת הלוגיקה של הסתעפות שמתחשבת בבחירה של המשתמש.
דוגמה להטמעה
מאחזרים את ההפניה שהוצעה באמצעות מזהה השיחה הפעילה, משלבים אותה עם CompanyResponse.preventDirectPstnCall ומציגים את ההנחיה.
lifecycleScope.launch {
val currentCall = CCAI.callService?.getLastCallInProgress()
?: return@launch
val deflection = CCAI.callService?.getCallDeflection(callId = currentCall.id)
?: return@launch
val company = CCAI.companyService?.get()
val allowDirectPstn = company?.preventDirectPstnCall != true
presentWaitTimeDeflection(deflection, allowDirectPstn)
}
אפשרויות מתקדמות CallOptions
CallOptions חושף קבוצה קטנה של מתגים לאפליקציות שצריכות לשנות את חוויית השיחה שמוגדרת כברירת מחדל.
connectingPollIntervalMs:Long– באיזו תדירות ה-SDK שולח שאילתות לפלטפורמה בזמן שהשיחה נמצאת במצב של התחברות.
connectedPollIntervalMs:Long– באיזו תדירות ה-SDK שולח בקשות לשרת הפלטפורמה בזמן שהשיחה מחוברת.
endCallMaxRetries:Int– כמה פעמים ה-SDK מנסה לשלוח מחדש את בקשת סיום השיחה לפני שמוצגת שגיאה.
endCallRetryDelayMs:Long– השהיה בין ניסיונות חוזרים לניתוק השיחה.
דוגמה להטמעה
מעבירים את הערכים המותאמים ל-CallOptions כשמאתחלים את מודול השיחות:
CCAI.initializeCall(
context = this,
options = CallOptions(
connectingPollIntervalMs = 2_000L,
connectedPollIntervalMs = 5_000L,
endCallMaxRetries = 3,
endCallRetryDelayMs = 1_000L
)
)
שיתוף המסך
שיתוף המסך מאפשר לנציגי התמיכה לראות את המסך של המשתמש כדי לעזור לו לפתור בעיות. פלטפורמת CCAI תומכת בשני מצבים של שיתוף מסך:
שיתוף מתוך האפליקציה: הסוכן יכול לראות רק את מה שקורה בתוך האפליקציה הספציפית שלכם.
שיתוף מלא של המכשיר (אופציונלי): הסוכן יכול לראות את כל המכשיר של המשתמש, כולל מסך הבית של Android ואפליקציות אחרות. כדי לעשות את זה, צריך להגדיר את Android בצורה מתקדמת באמצעות
MediaProjectionושירות פעיל.
מה ה-SDK מספק: מודול CCAIScreenShare שיוצר ומנהל סשנים, מטפל בשינויים במצב ומעבד בקשות להסכמה.
מה יוצרים: את הגדרות ההפעלה, את ההודעות בממשק המשתמש לבקשת הסכמה מהמשתמשים, ואם תומכים במכשיר מלא, את השילוב המובנה של Android MediaProjection עם שירות שפועל בחזית.
שירות שיתוף המסך מספק שיטות לכל מחזור החיים של הסשן – startSession, activateSession, stopSession – וגם שיטות להגדרת שליטה מרחוק ושיתוף של כל המכשיר. בקטעי המשנה הבאים מפורט תהליך האינטגרציה המרכזי.
אפשר לראות את החתימות המלאות של רכיבי ה-method ופרטים על הפרמטרים של כל רכיבי ה-method לשיתוף מסך במאמר בנושא Headless Mobile SDK – Android API Reference.
הפעלה והגדרה של שיתוף מסך
מאתחלים את שירות שיתוף המסך במהלך הגדרת האפליקציה על ידי הזנת מפתח שיתוף המסך:
import com.ccaiplatform.android.CCAI
import com.ccaiplatform.android.ScreenShareOptions
// Minimal - domain defaults to your instance's configured provider
CCAI.initializeScreenShare(
context = this,
screenShareOptions = ScreenShareOptions(
key = "YOUR_SCREEN_SHARE_KEY"
)
)
// If your instance or provider requires an explicit domain:
// CCAI.initializeScreenShare(
// context = this,
// screenShareOptions = ScreenShareOptions(
// key = "YOUR_SCREEN_SHARE_KEY",
// domain = "your_subdomain.ccaiplatform.com"
// )
// )
בדיקת הזכאות לשיתוף מסך
לפני שמפעילים את האפשרות לשיתוף מסך, צריך לבדוק אם היא זמינה בצ'אט הנוכחי:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
fun isScreenShareEnabled(chat: ChatResponse): Boolean {
return CCAI.screenShareService != null
&& chat.supportScreenShare == true
}
איך מטפלים בבקשות לשיתוף מסך
מטמיעים טיפול בבקשות לשיתוף מסך באמצעות ScreenShareManager. יוצרים סשן ומטפלים בתגובה האסינכרונית באמצעות ScreenShareCallbacks:
import com.ccaiplatform.ccaiscreenshare.ScreenShareManager
import com.ccaiplatform.ccaiscreenshare.ScreenShareCallbacks
fun requestScreenShare(chatId: String, isFromRemote: Boolean) {
val screenShareService = CCAI.screenShareService
if (screenShareService == null) {
Log.e("ScreenShare", "Screen share service not available")
return
}
// Track whether this session is agent-initiated
screenShareInitiatedFrom = if (isFromRemote) "agent" else "endUser"
if (!isFromRemote) {
sendScreenShareMessage(event = "screenShareRequestedFromEndUser")
}
val callbacks = ScreenShareCallbacks(
onSessionStateChanged = { state ->
Log.d("ScreenShare", "State changed: $state")
handleScreenShareStateChange(state)
},
onSessionCreationError = { error ->
Log.e("ScreenShare", "Session creation failed: ${error.message}")
sendScreenShareMessage(event = "screenShareFailed")
showErrorToast("Screen share failed to start.")
},
onSessionActivationRequest = {
Log.d("ScreenShare", "Activation request received")
// For agent-initiated sessions, activate immediately
// For user-initiated sessions, show a confirmation dialog first
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
} else {
showScreenShareConsentDialog {
ScreenShareManager.activateSession()
}
}
}
)
ScreenShareManager.startSession(
request = ScreenShareRequest(
communicationId = chatId,
communicationType = CommunicationType.Chat,
initiatedFrom = if (isFromRemote) ScreenShareFrom.AGENT else ScreenShareFrom.END_USER
),
callbacks = callbacks
)
}
טיפול בשינויים במצב שיתוף המסך
אפשר להאזין לשינויים במצב של סשן שיתוף המסך באמצעות הקריאה החוזרת onSessionStateChanged
שמסופקת ל-ScreenShareCallbacks:
fun handleScreenShareStateChange(state: ScreenShareSessionState) {
currentScreenShareSessionState = state
when (state) {
ScreenShareSessionState.INACTIVE -> {
Log.d("ScreenShare", "No active session")
}
ScreenShareSessionState.PENDING -> {
Log.d("ScreenShare", "Session pending - waiting for activation")
// For agent-initiated sessions, activate immediately
if (screenShareInitiatedFrom == "agent") {
ScreenShareManager.activateSession()
}
}
ScreenShareSessionState.ACTIVE -> {
Log.d("ScreenShare", "Screen share active")
sendScreenShareMessage(event = "screenShareStarted")
updateUiForActiveScreenShare()
}
}
}
ScreenShareCallbacks reference
| התקשרות חזרה | תיאור |
|---|---|
onSessionStateChanged |
הפונקציה נקראת כשמצב הסשן של שיתוף המסך משתנה (INACTIVE, PENDING, ACTIVE). |
onSessionCreationError |
הפונקציה מופעלת כשליצירת הסשן יש בעיה (בעיה ברשת, השירות לא זמין וכו'). |
onSessionActivationRequest |
הפונקציה נקראת כשהסשן מוכן להפעלה – מתקשרים אל ScreenShareManager.activateSession() כדי להתחיל בשיתוף. בסשנים שהמשתמש יזם, מוצג קודם פרומפט לבקשת הסכמה. בסשנים שהסוכן יזם, ההפעלה מתבצעת באופן מיידי. |
onSessionRemoteControlRequest |
הפונקציה מופעלת כשהסוכן מבקש שליטה מרחוק במסך של המשתמש. הצגת בקשת הסכמה לפני מתן שליטה מרחוק. |
onSessionFullDeviceRequest | הפונקציה מופעלת כשהסוכן מבקש לשתף את כל המכשיר (ראו שיתוף מסך של כל המכשיר בהמשך). מפעילים את תהליך ההסכמה של MediaProjection בתשובה. |
onSessionDidSucceed |
הפונקציה נקראת אחרי שהסשן מופעל בהצלחה והשידור מתחיל. אפשר להשתמש בערך הזה כדי לעדכן את ממשק המשתמש למצב 'שיתוף המסך פעיל'. |
הגדרות מלאות של סוגי ה-enum של המצב וכל הסוגים המשויכים מפורטות בהפניה ל-API של Headless Mobile SDK – Android.
שיתוף מסך של כל המכשיר (מתקדם)
שיתוף מסך של המכשיר כולו מאפשר לנציגים לראות מסכים מאפליקציות מחוץ לאפליקציה שלכם, כולל הגדרות המערכת וניווט בין אפליקציות.
כדי לעשות זאת, צריך לשלב את MediaProjection API של Android ולהפעיל שירות שפועל בחזית.
הצהרה על הרשאות ועל שירות שפועל בחזית
מוסיפים את ההרשאות הנדרשות ואת ההצהרה על שירותים שפועלים בחזית אל הקובץ AndroidManifest.xml:
<manifest>
<!-- Required for screen capture -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application>
<service
android:name=".screenshare.ScreenShareForegroundService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
</application>
</manifest>
בקשת הסכמה ל-MediaProjection
לפני שמצלמים את המסך, צריך לבקש מהמשתמש הסכמה באמצעות MediaProjectionManager:
import android.media.projection.MediaProjectionManager
import android.content.Context
private val mediaProjectionManager by lazy {
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
fun requestScreenCapturePermission() {
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureResultLauncher.launch(captureIntent)
}
// Handle the result in your Activity
private val screenCaptureResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
// User granted screen capture permission
startScreenShareForegroundService(result.resultCode, result.data!!)
} else {
Log.d("ScreenShare", "User denied screen capture permission")
}
}
הטמעה של השירות שפועל בחזית
יוצרים שירות שפועל בחזית ומחזיק את הסשן MediaProjection:
import android.app.Service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.IBinder
class ScreenShareForegroundService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
startForeground(NOTIFICATION_ID, notification)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Screen Share",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Active screen share session"
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
private fun buildNotification(): Notification {
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("Screen Sharing")
.setContentText("An agent is viewing your screen.")
.setSmallIcon(R.drawable.ic_screen_share)
.build()
}
companion object {
private const val CHANNEL_ID = "screen_share_channel"
private const val NOTIFICATION_ID = 1001
}
}
הפעלת שיתוף של כל המכשיר
אחרי שהאפשרות MediaProjection מופעלת, מפעילים שיתוף של כל המכשיר דרך שירות שיתוף המסך:
// After MediaProjection consent is granted and foreground service is started
ScreenShareManager.enableFullDeviceSharing(true)
שיקולים לגבי שיתוף מסך ב-Jetpack Compose
גישות מומלצות לאינטראקציה מרחוק ב-Jetpack פיתוח נייטיב
עוטפים רכיבים אינטראקטיביים מרכזיים בתצוגות Android – כדי שאפשר יהיה ללחוץ מרחוק על אזורים מסוימים, צריך לעטוף אותם כפקדים סטנדרטיים של תצוגות Android (לדוגמה,
android.widget.Button) באמצעות ה-composableAndroidView. כך אפשר לוודא שהקלקות מרחוק פועלות בצורה אמינה דרך הנתיב מבוסס-התצוגה של Android.העברת מגעים מרחוק עם טיפול מותאם אישית במגע – באזורים של Compose שלא ניתן להחליף ב-Views, משתמשים בהחזרות מותאמות אישית של מגע כדי להעביר אירועי מגע מרחוק ללוגיקה של Compose.
הצגת הנחיות או פתרונות חלופיים ברמת המוצר – לציין באופן ברור במסכי ההודעות ש "הסוכנים יכולים רק לצפות; המשתמש צריך להקיש באופן מקומי", או להגביל באופן אוטומטי את הרשאות השליטה מרחוק בדפים האלה תוך שמירה על גישה לצפייה בלבד.
דוגמה להטמעה של עטיפת תצוגה ב-Android
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import android.widget.Button
@Composable
fun RemoteControlButton(
title: String,
onClick: () -> Unit
) {
AndroidView(
factory = { context ->
Button(context).apply {
text = title
setOnClickListener { onClick() }
}
},
update = { button ->
button.text = title
}
)
}
// Usage in a Composable
@Composable
fun ScreenShareSupportScreen() {
Column {
Text("This screen supports remote control")
RemoteControlButton(title = "Clickable Button") {
println("Button tapped remotely or locally")
}
}
}
שיחות מתוזמנות
שיחות מתוזמנות מאפשרות למשתמשים להזמין שיחה קולית עתידית במקום לחכות בתור לשיחה מיידית. ב-Android, ערכת ה-SDK חושפת שיטות של שירותי שיחות לבדיקת זמינות, לאחזור משבצות זמן, להזמנת שיחות, לשינוי מועד של שיחות, לביטול שיחות ולטיפול בשגיאות בתזמון.
זיהוי יכולות
מה שערכת ה-SDK מספקת: דגל scheduleEnabled ב-VoiceCallChannel שמציין אם התור שנבחר תומך בהזמנת שיחות עתידיות.
מה יוצרים: לוגיקה לבדיקת הדגל הזה לפני הצגת נקודת הכניסה לתזמון.
דוגמה להטמעה
בודקים את scheduleEnabled בערוץ של שיחת הטלפון בתור. אל תשתמשו ב-voiceCall != null כפרוקסי – תור יכול לתמוך בשיחות קוליות מיידיות בלי לתמוך בשיחות מתוזמנות, ולהיפך.
fun setupScheduledCallEntryPoint(queueMenu: QueueMenu) {
val voiceChannel = queueMenu.channels.voiceCall
if (voiceChannel == null) {
callNowButton.isVisible = false
scheduleCallButton.isVisible = false
return
}
callNowButton.isVisible = voiceChannel.instantEnabled && voiceChannel.deflected != true
scheduleCallButton.isVisible = voiceChannel.scheduleEnabled == true
}
בדיקת זמינות אופציונלית
לפני שמוצג לוח הזמנים לבחירת משבצת זמן, אפשר לבדוק אם יש משבצות זמן פנויות בתור שנבחר.
דוגמה להטמעה
אפשר להשתמש ב-hasScheduledTimeSlots(menuId:) כבדיקה מקדימה קלה. אם הפונקציה מחזירה false, מוצג מצב ריק או אפשרות חלופית במקום לפתוח את הכלי לבחירת תמונות.
lifecycleScope.launch {
try {
val available = CCAI.callService?.hasScheduledTimeSlots(menuId = menu.id) == true
if (available) {
showScheduleCallUI()
} else {
showNoSlotsMessage()
}
} catch (e: Exception) {
Log.e("CCAI", "Availability check failed: ${e.message}")
showNoSlotsMessage()
}
}
אחזור משבצות זמן
אחזור של שעות פנויות לפגישות בתור שנבחר. ה-SDK מחזיר ערכי תאריך שהאפליקציה יכולה להציג לפי אזור הזמן המקומי של המשתמש.
מה ערכת ה-SDK מספקת: חותמות זמן של משבצות זמינות בתור שנבחר, עם תמיכה אופציונלית בתזמון מחדש של שיחה מתוזמנת קיימת.
מה יוצרים: את היומן, את הכלי לבחירת שעה, את הצגת אזור הזמן, את הטיפול במצב ריק ואת התנהגות הניסיון החוזר.
דוגמה להטמעה
מתקשרים אל getScheduledTimeSlots עם מזהה תפריט התור.
כשמזמנים מחדש, צריך להעביר את המזהה הקיים של השיחה המתוזמנת כדי שהשירות יוכל להחזיר את הזמינות המתאימה.
lifecycleScope.launch {
try {
val slots: List<Date> = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = null
) ?: emptyList()
if (slots.isEmpty()) {
showNoSlotsMessage()
} else {
displayTimeSlotPicker(slots)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to fetch time slots: ${e.message}")
}
}
כשמשנים את מועד השיחה, מעבירים את המזהה של השיחה הקיימת שנקבעה מראש:
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
יצירת שיחה מתוזמנת
אחרי שהמשתמש בוחר משבצת זמן, יוצרים את השיחה המתוזמנת דרך
callService.
מה ערכת ה-SDK מספקת: createScheduledCall API שקובע את המשבצת שנבחרה ומחזיר את רשומת השיחה המתוזמנת.
מה שאתם בונים: אימות של מספרי טלפון בפורמט E.164, ממשק משתמש לאישור, אחסון מקומי של מזהה השיחה המתוזמנת ושחזור במקרה של התנגשויות במשבצות זמן או תגובות אחרי שעות הפעילות.
דוגמה להטמעה
מעבירים את המשבצת שנבחרה, מספר הטלפון, מזהה התור ו-RecordingPermission.NOT_ASKED ב-ScheduledCallRequest.
lifecycleScope.launch {
try {
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = selectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = null,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to schedule call: ${e.message}")
}
}
מספר הטלפון צריך להיות בפורמט E.164, כמו +14155551234.
שינוי מועד של שיחה קיימת
כדי לקבוע מחדש את מועד השיחה, מאחזרים את המשבצות ויוצרים את השיחה המתוזמנת החדשה עם המזהה הקיים של השיחה המתוזמנת.
מה מספק ה-SDK: אותם ממשקי API של חלונות זמן ויצירה, עם callIdForRescheduling שמשמש לזיהוי ההזמנה הקיימת.
מה שתבנו: ממשק המשתמש של 'שעת השינוי', חיפוש מקומי של מזהה השיחה המתוזמנת המאוחסן ומצב האישור המעודכן אחרי יצירת ההזמנה החדשה.
דוגמה להטמעה
מעבירים את מזהה השיחה הקיים גם אל getScheduledTimeSlots וגם אל createScheduledCall.
lifecycleScope.launch {
try {
val slots = CCAI.callService?.getScheduledTimeSlots(
menuId = menu.id,
callIdForRescheduling = existingCallId
) ?: emptyList()
val response = CCAI.callService?.createScheduledCall(
ScheduledCallRequest(
menuId = menu.id,
phoneNumber = userPhoneNumber,
scheduleTime = newSelectedSlot,
recordingPermission = RecordingPermission.NOT_ASKED,
callIdForRescheduling = existingCallId,
customData = null,
ticketId = null
)
)
saveScheduledCallId(response?.id)
showConfirmation(scheduledAt = response?.scheduledAt)
} catch (e: CallCreationError.AfterHours) {
showMessage(e.displayMessage)
refreshTimeSlots()
} catch (e: Exception) {
Log.e("CCAI", "Failed to reschedule: ${e.message}")
}
}
ביטול שיחה מתוזמנת
כשהמשתמש מבטל שיחה קרובה, מתבצעת קריאה ל-call cancelScheduledCall והמזהה של השיחה שמאוחסן באופן מקומי נמחק.
מה ערכת ה-SDK מספקת: API לביטול של מזהה שיחה מתוזמנת.
מה יוצרים: הנחיה לאישור, ממשק משתמש של ביטול מוצלח או שגיאה, וניקוי של כל מצב של שיחה קרובה שמאוחסן באופן מקומי.
דוגמה להטמעה
מתקשרים אל cancelScheduledCall(callId:) עם המזהה המאוחסן, ואז מסירים את המזהה מהאחסון של האפליקציה.
lifecycleScope.launch {
try {
CCAI.callService?.cancelScheduledCall(callId = existingCallId)
clearScheduledCallId()
showCancellationConfirmation()
} catch (e: Exception) {
Log.e("CCAI", "Failed to cancel call: ${e.message}")
}
}
אימייל
מה ה-SDK מספק: כתובת האימייל של היעד שהוגדרה לתור הספציפי הזה, לצד הוראות שהאדמין רוצה שהמשתמש יראה (לדוגמה, יש לכלול את מספר החשבון).
מה בונים: מסך לכתיבת אימיילים והלוגיקה למעקב אחרי השלמת הכתיבה. אתם יכולים ליצור טופס מותאם אישית משלכם, או להשתמש בטופס המובנה של Android
Intent.ACTION_SENDTO כדי להפעיל את לקוח האימייל המועדף על המשתמש.
דוגמה להטמעה
הקוד הזה שולף את כתובת האימייל לתמיכה מאובייקט Channels של ה-SDK ב-QueueMenu. הוא פותח את תוכנת האימייל המובנית של Android ומטפל במקרים שבהם לא מותקנת תוכנת אימייל.
import android.content.Intent
import android.net.Uri
import android.widget.Toast
class SupportActivity : AppCompatActivity() {
// 1. Trigger the email flow
fun openEmailComposer(queueMenu: QueueMenu) {
val emailChannel = queueMenu.channels.email ?: return
val emailAddress = emailChannel.email ?: return
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(emailAddress))
putExtra(Intent.EXTRA_SUBJECT, "Support Request: ${queueMenu.name ?: ""}")
// If the queue provides an instruction message, include it in the body
// putExtra(Intent.EXTRA_TEXT, emailChannel.instructionMessage ?: "")
}
if (emailIntent.resolveActivity(packageManager) != null) {
emailResultLauncher.launch(emailIntent)
} else {
Toast.makeText(
this,
"No email client installed on this device.",
Toast.LENGTH_LONG
).show()
}
}
// 2. Track the outcome using ActivityResultContract
private val emailResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
// Note: Most Android email clients don't return a meaningful
// result code. Unlike iOS's MFMailComposeViewControllerDelegate,
// Android's email intent doesn't reliably report whether the
// email was sent, saved as draft, or cancelled.
// Log this event to your analytics and assume the user
// interacted with the email composer.
Log.d("CCAI", "Email composer closed. Result code: ${result.resultCode}")
MyAnalytics.logEvent("email_composer_closed")
}
}
קישורים להפניה חיצונית
מה ה-SDK מספק: רשימה של כתובות URL (כמו מרכז העזרה של החברה, מדיניות ההחזרים הכספיים או אפליקציית שותף) והכותרות לתצוגה שלהן, בדיוק כמו שהן מוגדרות בפורטל האדמין.
מה יוצרים: הלוגיקה להרצת הקישורים האלה כשמשתמש מקיש עליהם – או על ידי פתיחתם בדפדפן באפליקציה (באמצעות כרטיסיות מותאמות אישית של Android) או על ידי הפעלת דפדפן ברירת המחדל של המערכת.
דוגמה להטמעה (כרטיסיות מותאמות אישית – מומלץ)
כשמשתמש מקיש על קישור חיצוני מהתפריט שלכם, הקוד הזה מחלץ את מחרוזת כתובת ה-URL שסופקה על ידי ה-SDK ומשתמש בכרטיסיות מותאמות אישית של Chrome כדי להציג את דף האינטרנט בחוויית דפדפן באפליקציה. כרטיסיות מותאמות מספקות חוויה מהירה וחלקה יותר מאשר הפעלה של פונקציונליות דפדפן מלאה, ועדיין מציגות את סרגל הכתובות כדי שהמשתמש יידע שהוא צופה בתוכן חיצוני.
import androidx.browser.customtabs.CustomTabsIntent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(this, uri)
}
הטמעה של חלופה (דפדפן המערכת): אם כרטיסיות בהתאמה אישית לא זמינות או שאתם מעדיפים גישה פשוטה יותר, אתם יכולים לפתוח את הקישור בדפדפן ברירת המחדל של המשתמש:
import android.content.Intent
import android.net.Uri
fun openHelpCenterLink(urlString: String) {
val uri = Uri.parse(urlString) ?: return
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
showErrorToast("No browser available to open this link.")
}
}
דוגמה לשילוב מלא
הקוד הזה קורא את קישור ההפניה מהאובייקט Channels של ה-SDK ומציג אותו למשתמש כלחצן שאפשר להקיש עליו בתפריט הערוצים.
fun setupDeflectionLinks(queueMenu: QueueMenu) {
val deflectionLink = queueMenu.channels.externalDeflectionLink
if (deflectionLink != null) {
helpCenterButton.isVisible = true
helpCenterButton.setOnClickListener {
// Read the URL from the structured ExternalDeflectionLink type
val url = deflectionLink.url ?: return@setOnClickListener
openHelpCenterLink(url)
}
} else {
helpCenterButton.isVisible = false
}
}
ExternalDeflectionLink חושף גם שדות ברמה העליונה url / displayName
וגם מערך פנימי links: List<ExternalDeflection>?. בדוגמה שלמעלה מוצגת רק הרשומה ברמה העליונה, וזה מספיק לתורים של קישור יחיד. אם האדמינים שלכם מגדירים כמה קישורים חיצוניים בתור אחד, צריך להשתמש ב-deflectionLink.links כדי לעבור על כל רשומה מופעלת ולהציג כפתור אחד לכל רשומה – כל כפתור ExternalDeflection מכיל את הערכים url ו-displayName שלו:
deflectionLink.links
?.filter { it.enabled == true }
?.forEach { link ->
val button = MaterialButton(this).apply {
text = link.displayName
setOnClickListener { openHelpCenterLink(link.url) }
}
helpCenterContainer.addView(button)
}