Activitate de Laborator

Se dorește implementarea unei aplicații Android care folosește stiva Android Linphone pentru a realiza apeluri de voce precum și comunicație prin mesagerie instantanee folosind SIP.

Proiectul conține o singură activitate cu două layouturi care primesc alternativ vizibilitatea View.GONE și View.VISIBLE pentru a apărea și a dispărea din spațiul activitătii curente. Varianta inițială a scheletului schimbă între cele două layouturi folosind butoanele Register/Unregister.

1. În contul Github personal, să se creeze un depozit denumit 'Laborator10'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la .

În urma acestei operații, directorul Laborator10 va trebui să se conțină directorul labtasks.

student@eim-lab:~$ git clone https://github.com/eim-lab/Laborator10.git

Pentru java, comutați pe branchul master-java:

student@eim-lab:~$ git checkout -b master-java

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator10' de pe contul Github personal.

student@eim-lab:~$ cd Laborator10
student@eim-lab:~/Laborator10$ git remote add Laborator10_perfectstudent https://github.com/perfectstudent/Laborator10
student@eim-lab:~/Laborator10$ git push Laborator10_perfectstudent master

4. Să se importe în mediul integrat de dezvoltare Android Studio proiectul labtasks/siplinphone.

Credențiale SIP în proiectul Android

Puteți pune credențialele de conectare în fișierul app/src/main/res/values/strings.xml

<resources>
    <string name="mysipname">eim-lab</string>
    <string name="mysipdomain">some_server.sip</string>
    <string name="mysippassword">PaSsWoRd</string>
</resources>

cu condiția să îl scoateți din git, pentru a nu trimite accidental credențialele în repo:

$ cp solutions/SIPLinphone/app/src/main/res/values/strings.xml /tmp
$ git rm solutions/SIPLinphone/app/src/main/res/values/strings.xml
$ mv /tmp/strings.xml solutions/SIPLinphone/app/src/main/res/values/

5. În activitatea MainActivity, layoutul principal, să se implementeze ascultători pentru butoanele Register și Unregister:

  • Register

    1. va configura motorul Linphone, ai cărui parametri sunt plasați sub forma unor valori asociate unor chei
    2. va porni motorul Linphone (metoda start())

    Aceste funcționalități for fi grupate în funcția login() care poate fi apelată din listener

    private fun login() {
        val username = findViewById<EditText>(R.id.username).text.toString()
        val password = findViewById<EditText>(R.id.password).text.toString()
        val domain = findViewById<EditText>(R.id.domain).text.toString()
        val transportType = when (findViewById<RadioGroup>(R.id.transport).checkedRadioButtonId) {
            R.id.udp -> TransportType.Udp
            R.id.tcp -> TransportType.Tcp
            else -> TransportType.Tls
        }
        val authInfo = Factory.instance().createAuthInfo(username, null, password, null, null, domain, null)
    
        val params = core.createAccountParams()
        val identity = Factory.instance().createAddress("sip:$username@$domain")
        params.identityAddress = identity
    
        val address = Factory.instance().createAddress("sip:$domain")
        address?.transport = transportType
        params.serverAddress = address
        params.setRegisterEnabled(true)
        val account = core.createAccount(params)
    
        core.addAuthInfo(authInfo)
        core.addAccount(account)
    
        core.defaultAccount = account
        core.addListener(coreListener)
        core.start()
    
        if (packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, packageName) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 0)
            return
        }
    }
    
    
    • Unregister *
     findViewById<Button>(R.id.unregister).setOnClickListener {
            val account = core.defaultAccount
            if(account != null) {
                val params = account.params
                val clonedParams = params.clone()
                clonedParams.setRegisterEnabled(false)
                account.params = clonedParams
                it.isEnabled = false
            }
        }
    

    Succesul acestor operații nu poate fi vizualizat imediat, deoarece aceste rezultate vor fi furnizate prin callback-uri ale obiectului corelistener care va fi definit ca mebru privat al activității:

    class MainActivity:  AppCompatActivity() {
      private lateinit var core: Core
      private val coreListener = object: CoreListenerStub() {
        override fun onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState?, message: String) {
          ...
        }
        override fun onCallStateChanged(core: Core, call: Call, state: Call.State?, message: String) {
          ...
        }
    

Pentru proiectul de astăzi, coreListener va primi callback-uri legate de starea procedurilor de REGISTER, și a stării apelului (incoming sau outgoing).

6. Să se implementeze callback-urile din coreListener pentru starea înregistrării în care se activează și se dezactivează butoanele sau layouturile:

override fun onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState?, message: String) {
            when (state) {
                RegistrationState.Failed -> {
                    findViewById<Button>(R.id.register).isEnabled = true
                }

                RegistrationState.Cleared -> {
                    findViewById<LinearLayout>(R.id.register_layout).visibility = View.VISIBLE
                    findViewById<RelativeLayout>(R.id.call_layout).visibility = View.GONE
                    findViewById<Button>(R.id.register).isEnabled = true
                }

                RegistrationState.Ok -> {
                    findViewById<LinearLayout>(R.id.register_layout).visibility = View.GONE
                    findViewById<RelativeLayout>(R.id.call_layout).visibility = View.VISIBLE
                    findViewById<Button>(R.id.unregister).isEnabled = true
                    findViewById<EditText>(R.id.remote_address).isEnabled = true
                }
                else -> {}
            }
}

7. Sa se adauge în OnCreate listenere pentru butoanele Call și Hang-up

  findViewById<Button>(R.id.call).setOnClickListener {
            outgoingCall()
            findViewById<EditText>(R.id.remote_address).isEnabled = false
            findViewById<Button>(R.id.hang_up).isEnabled = true
            it.isEnabled = false
  }

   private fun outgoingCall() {  
        val remoteSipUri = findViewById<EditText>(R.id.remote_address).text.toString()
        val remoteAddress = Factory.instance().createAddress("sip:$remoteSipUri")
        remoteAddress ?: return 
        val params = core.createCallParams(null)
        params ?: return 
        params.mediaEncryption = MediaEncryption.None
        // initiate call, but status will be in coreListener:onCallStateChanged
        core.inviteAddressWithParams(remoteAddress, params)
    }

  findViewById<Button>(R.id.hang_up).setOnClickListener {

            findViewById<EditText>(R.id.remote_address).isEnabled = true
            findViewById<Button>(R.id.call).isEnabled = true

            if (core.callsNb != 0) {
                val call = if (core.currentCall != null) core.currentCall else core.calls[0]
                if(call != null)
                    call.terminate()
            }
  }

În acest moment pot fi testate apeluri către roboți de SIP, cum ar fi:

  • 904@mouselike.org
  • thetestcall@sip.linphone.org

8. Să se implementeze callback-urile din coreListener pentru starea apelului:

override fun onCallStateChanged(core: Core, call: Call, state: Call.State?, message: String) {
            when (state) {
                Call.State.IncomingReceived -> {
                    findViewById<Button>(R.id.hang_up).isEnabled = true
                    findViewById<Button>(R.id.answer).isEnabled = true
                }
                Call.State.Connected -> {
                    findViewById<Button>(R.id.mute_mic).isEnabled = true
                    findViewById<Button>(R.id.toggle_speaker).isEnabled = true
                    Toast.makeText(this@MainActivity, "remote party answered",  Toast.LENGTH_LONG).show()
                }
                Call.State.Released -> {
                    findViewById<Button>(R.id.hang_up).isEnabled = false
                    findViewById<Button>(R.id.answer).isEnabled = false
                    findViewById<Button>(R.id.mute_mic).isEnabled = false
                    findViewById<Button>(R.id.toggle_speaker).isEnabled = false
                    findViewById<EditText>(R.id.remote_address).text.clear()
                    findViewById<Button>(R.id.call).isEnabled = true
                }

                else -> {}
            }
        }

Pentru a accepta apeluri, trebuie activat butonul Answer cu un apel la core:

        findViewById<Button>(R.id.answer).setOnClickListener {
            core.currentCall?.accept()
        }

În acest moment pot fi testate apeluri între diverse instanțe ale laboratorului, sau comunica cu un alt client SIP.

9. Să se analizeze conversația SIP la nivel pachet folosind un apel de voce către o adresă SIP de test (thetestcall@sip.linphone.org). Pentru a captura pachetele în device este nevoie de root (telefon rutat, imagine root-ată).

Aveți root pe device

Aveți root pe device

    student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb -s 192.168.56.101:5555 shell

În consola sistemului de operare Android, se folosește utilitarul tcpdump pentru monitorizarea traficului de pachete.

Binarele pentru acest utilitar, precompilate pentru sisteme de operare Android, folosind arhitecturi ARM, pot fi descărcate de pe Android TCP Dump.

În situația în care este necesar ca acest utilitar să fie instalat pe alte arhitecturi (de exemplu, Genymotion folosește x86), binarul acestuia poate fi obținut folosind utilitarul build-android-tcpdump care însă are nevoie de NDK precum și de alte programe (flex, bison).

Transferul binarului tcpdump de pe mașina fizică pe dispozitivul mobil (rootat) sau pe emulator se face astfel:

    student@eim-lab:/android/sdk/platform-tools$ ./adb -s 192.168.65.101:5555 push tcpdump /data/bin

Notă:

Utilitarul tcpdump se instalează în /data/bin, apoi se conferă drepturi de execuție pentru binar:

root@android:/data/bin# chmod 777 tcpdump


Monitorizarea propriu-zisă a pachetelor UDP pe interfața de rețea eth1 poate fi realizată prin intermediul următoarei comenzi:

root@android:/# ./tcpdump -s0 -ni eth1 -w /sdcard/DCIM/sip.pcap 'udp'

Se pornește apelul audio și după ce se termină mesajul, se oprește.

Programul tcpdump este terminat prin Ctrl-C.

student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb -s 192.168.56.101:5555 pull /sdcard/DCIM/sip.pcap
student@eim-lab:/opt/android-sdk-linux/platform-tools$ wireshark sip.pcap

Nu aveți root pe device

Va trebui să capturați pachetele de tip UDP din mașina de dezvoltare care funcționează ca un ruter pentru device-ul virtual. Dacă folosiți un device real, și acesta va trimite pachetele VoiP/SIP prin interfeța 4G, atunci nu vor putea fi capturate în mașina de dezvoltare. Pentru pachetele trimise de pe device prin WiFi se poate face o captură numai dacă WiFi nu folosește WPA și pachetele zboară in clear (nu sunt criptate prin aer).

Linux
$ ip ro 
default via 192.168.100.1 dev enp0s31f6 proto dhcp src 192.168.100.107 metric 100 
172.17.17.0/24 dev ztrf26yfym proto kernel scope link src 172.17.17.18 
172.18.0.0/16 dev docker0 proto kernel scope link src 172.18.0.1 linkdown 
192.168.56.0/24 dev vboxnet0 proto kernel scope link src 192.168.56.1 linkdown 
192.168.100.0/24 dev enp0s31f6 proto kernel scope link src 192.168.100.107 metric 100 

interfața default a mașinii de dezvoltare este enp0s31f6, așadar aici pot fi capturate cu wireshark pachetele forwardate pentru device-ul virtual.

Windows Windows: TODO - instalare wireshark și captură fie din aer, fie dacă rulăm AVD/geny de pe interfața default
OSX OSX: TODO - utilitarul airport, instalare wireshark

Se obține dump-ul fie din dispozitivul virtual, fie din masina de dezvoltare și se analizează folosind wireshark instalat local.

  • Să se identifice operația REGISTER. Ce port se utilizează? Care este adresa serverului?

  • Să se găsească, în răspunsul de confirmare, adresele NAT prin care trece conversația, odată ce a fost acceptată cererea.

  • Să se identifice operația INVITE. Apar retransmisii?

  • Ce fel de codificare este utilizată pentru semnalul audio?

  • Ce parametri are fluxul de voce (protocol, dimensiune pachet, rata pachetelor)?

  • Ce adrese sunt folosite pentru traficul de voce și cum au fost negociate?


Note

Pornirea monitorizării (pornirea utilitarului tcpdump) trebuie realizată anterior operației de înregistrare. Similar, oprirea monitorizării trebuie realizată ulterior operației de deînregistrare. În acest fel, pot fi surprinse toate operațiile.


10. (opțional) Pentru a trimite coduri numerice DTMF (Dual Tone Multi Frequency) se creează un buton și un câmp text editabil asociat. Transmiterea unui astfel de caracter se realizează prin intermediul metodei sendDtmf() a obiectului core.currentCall cu valorile întregi 0-9, sau 10 pentru * și 11 pentru #. Folosind o adresa de test (thetestcall@sip.linphone.org sau 904@mouselike.org) să se testeze codurile și navigarea prin meniuri.

Să se implementeze metoda asociată clasei ascultător corespunzătoare operației de apăsare a butonului respectiv.

Indicații de Rezolvare
  findViewById<Button>(R.id.dtmfsend).setOnClickListener {
              val keypress = (findViewById<EditText>(R.id.dtmfedit)).text.toString()
              if(keypress.isEmpty()){
                  Toast.makeText(this@MainActivity, "Need phone key character 0-9, +, #",  Toast.LENGTH_LONG).show()
                  return@setOnClickListener
              }

              val call = if (core.currentCall != null)
                    core.currentCall
                else if( core.calls.size > 0)
                    core.calls[0]
                else null
              if(call != null)
                  call.sendDtmf(keypress[0])
  }

11. (Opțional) TODO Implementare instant messenging. Activitatea InstantMessagingActivity poate fi lansată din activitatea principală, doar ulterior operației de înregistrare. Aceasta primește ca argument, în intenția cu care este lansată în execuție, adresa SIP cu care se va desfașura sesiunea de mesagerie instantanee.

12. Să se încarce modificările realizate în cadrul depozitului 'Laborator10' de pe contul Github personal, folosind un mesaj sugestiv.

student@eim-lab:~/Laborator10$ git add * # dar ștergeți credențialele 
student@eim-lab:~/Laborator10$ git commit -m "implemented taks for laboratory 10"
student@eim-lab:~/Laborator10$ git push Laborator10_perfectstudent master

Resurse Utile

Introduction to SIP - A Beginners' Tutorial as part of Internet Multimedia
How VoIP Works?
Session Initiation Protocol (Tutorial's Point)
Session Initiation Protocol - Wikipedia
Linphone app
WebRTC