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
- va configura motorul Linphone, ai cărui parametri sunt plasați sub forma unor valori asociate unor chei
- 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 defaultOSX
OSX: TODO - utilitarul airport, instalare wiresharkSe 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