Core-Telecom

ספריית Core-Telecom מייעלת את תהליך השילוב של אפליקציית השיחות עם פלטפורמת Android, באמצעות סט חזק ועקבי של ממשקי API

אם אתם רוצים לראות יישומים מעשיים, תוכלו למצוא אפליקציות לדוגמה ב-GitHub:

הגדרת Core-Telecom

מוסיפים את התלות androidx.core:core-telecom לקובץ build.gradle של האפליקציה:

dependencies {
    implementation ("androidx.core:core-telecom:1.0.0")
}

מצהירים על ההרשאה MANAGE_OWN_CALLS בקובץ AndroidManifest.xml:

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

רישום האפליקציה

כדי להתחיל להוסיף שיחות למערכת, צריך לרשום את אפליקציית השיחות ב-Android באמצעות CallsManager. במהלך ההרשמה, מציינים את היכולות של האפליקציה (לדוגמה, תמיכה באודיו, בווידאו):

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

ניהול השיחות

משתמשים בממשקי Core-Telecom API כדי ליצור ולנהל את מחזור החיים של שיחה.

יצירת שיחה

אובייקט CallAttributesCompat מגדיר את המאפיינים של שיחה ייחודית, שיכולים להיות לה המאפיינים הבאים:

  • displayName: שם המתקשר.
  • address: כתובת לשיחה (לדוגמה, מספר טלפון, קישור לפגישה ב-Meet).
  • direction: נכנס או יוצא.
  • callType: אודיו או וידאו.
  • callCapabilities: תומך בהעברה ובהחזקה לצורך משפטי.

דוגמה ליצירת שיחה נכנסת:

fun createIncomingCallAttributes(
    callerName: String,
    callerNumber: String,
    isVideoCall: Boolean): CallAttributesCompat {
    val addressUri = Uri.parse("YourAppScheme:$callerNumber")

    // Define capabilities supported by your call.
    val callCapabilities = CallAttributesCompat.CallCapability(
        supportsSetInactive = CallAttributesCompat.SUPPORTS_SET_INACTIVE // Call can be made inactive (implies hold)
    )

    return CallAttributesCompat(
        displayName = callerName,
        address = addressUri,
        direction = CallAttributesCompat.DIRECTION_INCOMING,
        callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
        callCapabilitiesCompat = callCapabilities
    )
}

הוספת שיחה

אפשר להשתמש ב-callsManager.addCall עם CallAttributesCompat ועם קריאות חוזרות כדי להוסיף שיחה חדשה למערכת ולנהל עדכונים של ממשקים מרוחקים. ההרשאה callControlScope בתוך הבלוק addCall מאפשרת לאפליקציה בעיקר להעביר את מצב השיחה ולקבל עדכוני אודיו:

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onAnswerCall, // Watch needs to know if it can answer the call.
        onSetCallDisconnected,
        onSetCallActive,
        onSetCallInactive
    ) {
        // The call was successfully added once this scope runs.
        callControlScope = this
    }
}
catch(addCallException: Exception){
   // Handle the addCall failure.
}

מענה לשיחה

מענה לשיחה נכנסת תוך CallControlScope:

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

דחיית שיחה

דחיית שיחה באמצעות disconnect() עם DisconnectCause.REJECTED בתוך CallControlScope:

disconnect(DisconnectCause(DisconnectCause.REJECTED))

הפעלה של שיחה יוצאת

הגדרת שיחה יוצאת כפעילה ברגע שהצד השני עונה:

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

העברת שיחה להמתנה

כדי להעביר שיחה למצב המתנה באמצעות setInactive():

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

ניתוק שיחה

ניתוק שיחה באמצעות disconnect() עם DisconnectCause:

disconnect(DisconnectCause(DisconnectCause.LOCAL))

ניהול נקודות קצה של אודיו בשיחות

אפשר לצפות בנקודות קצה של אודיו ולנהל אותן באמצעות currentCallEndpoint, ‏availableEndpoints ו-isMuted Flow בתוך CallControlScope

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

שינוי מקור האודיו הפעיל באמצעות requestEndpointChange():

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

תמיכה בשירותים שפועלים בחזית

הספרייה משתמשת ב-ConnectionService (Android מגרסה 13, רמת API 33 ומטה) או ב-foregroundtypes (Android מגרסה 14, רמת API 34 ומעלה) לתמיכה בפעילות בחזית.

כחלק מהדרישות לגבי פעילות בחזית, האפליקציה צריכה להציג למשתמשים התראה כדי ליידע אותם שהאפליקציה פועלת בחזית.

כדי לוודא שהאפליקציה מקבלת עדיפות להפעלה בחזית, צריך ליצור התראה אחרי שמוסיפים את השיחה לפלטפורמה. העדיפות של הפעילות בחזית מוסרת כשהאפליקציה מסיימת את השיחה או כשההתראה כבר לא תקפה.

מידע נוסף על שירותים שפועלים בחזית

תמיכה מרחוק ב-Surface

מכשירים מרוחקים (שעונים חכמים, אוזניות Bluetooth, ‏ Android Auto) יכולים לנהל שיחות בלי אינטראקציה ישירה עם הטלפון. באפליקציה צריך להטמיע פונקציות callback מסוג lambda‏ (onAnswerCall, onSetCallDisconnected, onSetCallActive,‏ onSetCallInactive) שמועברות אל CallsManager.addCall כדי לטפל בפעולות שמתבצעות במכשירים האלה.

כשמתרחשת פעולה מרחוק, מופעלת פונקציית ה-lambda המתאימה.

השלמה מוצלחת של ה-lambda מציינת שהפקודה עברה עיבוד. אם אי אפשר לבצע את הפקודה, פונקציית ה-lambda צריכה להחזיר חריגה.

הטמעה נכונה מבטיחה שליטה חלקה בשיחות במכשירים שונים. חשוב לבצע בדיקות מקיפות במגוון ממשקים מרוחקים.

תוספי שיחה

בנוסף לניהול מצב השיחה וניתוב האודיו של השיחות, הספרייה תומכת גם בתוספים לשיחות. אלה תכונות אופציונליות שאפשר להטמיע באפליקציה כדי לשפר את חוויית השיחות בממשקים מרוחקים, כמו Android Auto. התכונות האלה כוללות חדרי ישיבות, השתקת שיחות וסמלי שיחות נוספים. כשמטמיעים תוסף באפליקציה, המידע שהאפליקציה מספקת מסתנכרן עם כל המכשירים המחוברים שתומכים גם בהצגת התוספים האלה בממשק המשתמש שלהם. המשמעות היא שהתכונות האלה יהיו זמינות גם במכשירים מרוחקים, כדי שהמשתמשים יוכלו ליצור איתן אינטראקציה.

יצירת שיחה עם תוספים

כשיוצרים שיחה, במקום להשתמש ב-CallManager#addCall כדי ליצור את השיחה, אפשר להשתמש ב-CallManager#addCallWithExtensions, שנותן לאפליקציה גישה להיקף שונה שנקרא ExtensionInitializationScope. היקף ההרשאה הזה מאפשר לאפליקציה לאתחל את קבוצת התוספים האופציונליים שהיא תומכת בהם. בנוסף, ההיקף הזה מספק שיטה נוספת, onCall, שמספקת CallControlScope בחזרה לאפליקציה אחרי שמתבצע חילוף של יכולות ההרחבה והאתחול מסתיים.

scope.launch {
    mCallsManager.addCallWithExtensions(
        attributes,
        onAnswer,
        onDisconnect,
        onSetActive,
        onSetInactive
    ) {
        // Initialize extension-specific code...

        // After the call has been initialized, perform in-call actions
        onCall {
            // Example: process call state updates
            callStateFlow.onEach { newState ->
                // handle call state updates and notify telecom
            }.launchIn(this)

            // Use initialized extensions...
        }
    }
}

תמיכה במשתתפים בשיחה

אם האפליקציה שלכם תומכת במשתתפים בפגישות או בשיחות קבוצתיות, צריך להשתמש ב-addParticipantExtension כדי להצהיר על תמיכה בתוסף הזה, ולהשתמש בממשקי ה-API שקשורים אליו כדי לעדכן את הממשקים המרוחקים כשהמשתתפים משתנים.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial participants state in the call.
        val participantExtension = addParticipantExtension(
            initialParticipants,
            initialActiveParticipant
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
        }
    }

בנוסף להודעה לממשקים מרוחקים על המשתתפים בשיחה, אפשר לעדכן את המשתתף הפעיל באמצעות ParticipantExtension#updateActiveParticipant.

יש גם תמיכה בפעולות אופציונליות שקשורות למשתתפים בשיחה. האפליקציה יכולה להשתמש ב-ParticipantExtension#addRaiseHandSupport כדי לתמוך ברעיון של משתתפים שמצביעים בשיחה ולראות אילו משתתפים נוספים גם מצביעים.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial list of participants in the call.
        val participantExtension = addParticipantExtension(initialParticipants)
        // Notifies Jetpack that this app supports the notion of participants
        // being able to raise and lower their hands.
        val raiseHandState = participantExtension.addRaiseHandSupport(
                initialRaisedHands
            ) { onHandRaisedStateChanged ->
                // handle this user's raised hand state changed updates from
                // remote surfaces.
            }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
            // notify remote surfaces of which of the participants have their
            // hands raised
            raisedHandsFlow.onEach { newRaisedHands ->
                raiseHandState.updateRaisedHands(newRaisedHands)
            }.launchIn(this)
        }
    }

השתקת שיחות תמיכה

התכונה'השתקת שיחות' מאפשרת למשתמש לבקש מהאפליקציה להשתיק את האודיו היוצא של השיחה בלי להשתיק פיזית את המיקרופון של המכשיר. התכונה הזו מנוהלת לכל שיחה בנפרד, כך ש-Jetpack מטפל במורכבות של ניהול מצב ההשתקה הגלובלי של שיחות סלולריות פעילות בזמן ששיחת VoIP פעילה. כך קל יותר להשתיק את האודיו היוצא בתרחישים של כמה שיחות, וגם להשתמש בתכונות מועילות כמו אינדיקציות לשאלה 'האם אתה מדבר?' כשהמשתמש מדבר בלי להבין שההשתקה של השיחה מופעלת.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for locally silencing the call's outgoing audio and
        // register a handler for when the user changes the call silence state
        // from a remote surface.
        val callSilenceExtension = addLocalCallSilenceExtension(
            initialCallSilenceState = false
        ) { newCallSilenceStateRequest ->
            // handle the user's request to enable/disable call silence from
            // a remote surface
        }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's call silence state changes, update remote
            // surfaces of the new state.
            callSilenceState.onEach { isSilenced ->
                callSilenceExtension.updateIsLocallySilenced(isSilenced)
            }.launchIn(this)
        }
    }

סמלים של שיחות תמיכה

סמל של שיחה מאפשר לאפליקציה לציין סמל מותאם אישית שמייצג את השיחה ויוצג בפלטפורמות מרוחקות במהלך השיחה. הסמל הזה יכול להתעדכן גם במהלך השיחה.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for a custom call icon to be displayed during the
        // lifetime of the call.
        val callIconExtension = addCallIconExtension(
            initialCallIconUri = initialUri
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's icon changes, update remote surfaces by providing
            // the new URI.
            callIconUri.onEach { newIconUri ->
                callIconExtension.updateCallIconUri(newIconUri)
            }.launchIn(this)
        }
    }

שילוב עם יומן השיחות של המערכת

‫Core-Telecom מאפשר לאפליקציה שלכם להשתלב עם יומן השיחות של המערכת. התכונה הזו מאפשרת לשיחות שמתבצעות או מתקבלות באמצעות אפליקציית ה-VoIP להופיע בהיסטוריית השיחות של החייגן במערכת, וכך המשתמשים יכולים להשתמש בחייגן כדי ליזום שיחה חוזרת.

הפעלת שילוב של יומן שיחות

כדי להפעיל את התכונה הזו, האפליקציה צריכה להירשם לטיפול ב-intent‏ TelecomManager.ACTION_CALL_BACK ב-AndroidManifest.xml ולספק Activity לעיבוד הקריאה החוזרת. לדוגמה, אם VoipCallbackActivity היא הפעילות שמטפלת בקריאה החוזרת, קובץ המניפסט צריך להיראות כך:

רישום הכוונה במניפסט

<activity
    android:name=".VoipCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.action.CALL_BACK" />
    </intent-filter>
</activity>

טיפול ב-intent להתקשרות חזרה

כשהמשתמש יוזם שיחה חוזרת מחייגן המערכת, הפעילות הרשומה שלכם מופעלת עם הפעולה TelecomManager.ACTION_CALL_BACK. הכוונה מכילה את TelecomManager.EXTRA_UUID שסופק לאפליקציה כשבמקור הוספתם את השיחה באמצעות CallsManager.

השיטה CallControlScopegetCallId מחזירה את המזהה הייחודי הזה של השיחה.

כדאי לשמור באפליקציה את כל המידע שנדרש כדי להתחיל את השיחה שמזוהה על ידי ה-UUID (לדוגמה, רשימה של מספרי טלפון לשיחה קבוצתית), כדי שתוכלו לשחזר את השיחה ולבצע אותה שוב כשהקריאה החוזרת מופעלת.

class VoipCallbackActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (intent?.action == TelecomManager.ACTION_CALL_BACK) {
            val uuidString = intent.getStringExtra(TelecomManager.EXTRA_UUID)
            if (uuidString != null) {
                val uuid = UUID.fromString(uuidString)
                
                // Retrieve your persisted call info using the UUID
                val callData = CallRepository.getCallByUuid(uuid)
                
                if (callData != null) {
                    // Place the call again using your app's logic
                    // ...
                }
            }
        }
        finish()
    }
}

ביטול ההסכמה לשילוב של יומן השיחות

כברירת מחדל, השיחות מתועדות ביומן השיחות של המערכת. אתם יכולים להשבית את התכונה הזו לכל שיחה בנפרד על ידי הגדרת הפרמטר isLogged לערך false כשאתם יוצרים את CallAttributesCompat:

val callAttributes = CallAttributesCompat(
    displayName = callerName,
    address = addressUri,
    direction = CallAttributesCompat.DIRECTION_OUTGOING,
    isLogExcluded = true // Opt-out of system call logging
)