Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4675)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/account.py	(revision 4677)
@@ -69,5 +69,5 @@
 	
 	def newChat(self, buddy):
-		chat = ch.Chat(self, buddy)
+		chat = ch.Chat(self.app, self, buddy)
 		self.chatList.append(chat)
 		return chat
@@ -109,16 +109,19 @@
 		ci = c.getInfo()
 		msg = "Incoming call for account '%s'" % self.cfg.idUri
-		if msgbox.askquestion(msg, "Accept call from '%s'?" % (ci.remoteURI), default=msgbox.YES) == u'yes':
+		if msgbox.askquestion(msg, "Accept call from '%s'?" % (ci.remoteUri), default=msgbox.YES) == u'yes':
 			call_prm.statusCode = 200
 			c.answer(call_prm)
 			
 			# create chat instance
-			bud = self.findBuddy(ci.remoteURI)
-			if not bud: return
+			bud = self.findBuddy(ci.remoteUri)
+			if not bud:
+				print "=== Incoming call from '%s': cannot find buddy" % ci.remoteUri
+				return
 			chat = self.findChat(bud)
-			if not chat: chat = self.newChat(bud, c)
-			
+			if not chat: chat = self.newChat(bud)
+			
+			chat.showWindow()
 			chat.registerCall(bud, c)
-			chat.showWindow()
+			chat.updateCallState(c, ci)
 		else:
 			c.hangup(call_prm)
@@ -126,10 +129,12 @@
 	def onInstantMessage(self, prm):
 		bud = self.findBuddy(prm.fromUri)
-		if not bud: return
+		if not bud:
+			print "=== Incoming IM from '%s': cannot find buddy" % prm.fromUri
+			return
 		chat = self.findChat(bud)
 		if not chat: chat = self.newChat(bud)
 			
+		chat.showWindow()
 		chat.addMessage(bud.cfg.uri, prm.msgBody)
-		chat.showWindow()
 		
 	def onInstantMessageStatus(self, prm):
@@ -145,5 +150,7 @@
 	def onTypingIndication(self, prm):
 		bud = self.findBuddy(prm.fromUri)
-		if not bud: return
+		if not bud:
+			print "=== Incoming typing indication from '%s': cannot find buddy" % prm.fromUri
+			return
 		chat = self.findChat(bud)
 		if not chat: return
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4675)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/application.py	(revision 4677)
@@ -58,5 +58,5 @@
 		
 		# GUI variables
-		self.showLogWindow = tk.IntVar(value=1)
+		self.showLogWindow = tk.IntVar(value=0)
 		self.quitting = False 
 		
@@ -378,5 +378,5 @@
 			if not chat: chat = acc.newChat(bud)
 			chat.showWindow()
-			chat.startAudio()
+			chat.startCall()
 		elif label=='Send instant message':
 			chat = acc.findChat(bud)
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/call.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/call.py	(revision 4675)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/call.py	(revision 4677)
@@ -51,4 +51,13 @@
 			self.chat.updateCallState(self, ci)
 			
+	def onCallMediaState(self, prm):
+		ci = self.getInfo()
+		for mi in ci.media:
+			if mi.type == pj.PJMEDIA_TYPE_AUDIO and \
+			  (mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE or \
+			   mi.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD):
+				m = self.getMedia(mi.index)
+				print m
+			
 	def onInstantMessage(self, prm):
 		# chat instance should have been initalized
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chat.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chat.py	(revision 4675)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chat.py	(revision 4677)
@@ -32,5 +32,6 @@
 	
 class Chat(gui.ChatObserver):
-	def __init__(self, acc, bud, call_inst=None):
+	def __init__(self, app, acc, bud, call_inst=None):
+		self._app = app
 		self._acc = acc
 		self._participantList = []
@@ -54,4 +55,19 @@
 		return the_call
 		
+	def _getBuddyFromUri(self, uri):
+		for bud in self._participantList:
+			if uri == bud.cfg.uri:
+				return bud
+		return None
+		
+	def _getCallFromUri(self, uri, op = ''):
+		for idx, bud in enumerate(self._participantList):
+			if uri == bud.cfg.uri:
+				if idx < len(self._callList):
+					return self._callList[idx]
+				return None
+		print "=== %s cannot find buddy URI '%s'" % (op, uri)
+		return None
+		
 	def _sendTypingIndication(self, is_typing):
 		type_ind_param = pj.SendTypingIndicationParam()
@@ -60,5 +76,5 @@
 			c = self._getCallFromBuddy(bud)
 			try:
-				if c:
+				if c and c.connected:
 					c.sendTypingIndication(type_ind_param)
 				else:
@@ -67,8 +83,27 @@
 				pass
 
+	def _sendInstantMessage(self, msg, sender_uri=''):
+		send_im_param = pj.SendInstantMessageParam()
+		send_im_param.content = str(msg)
+		for bud in self._participantList:
+			# don't echo back to the original sender
+			if sender_uri and bud.cfg.uri == sender_uri:
+				continue
+				
+			# send via call, if any, or buddy
+			c = self._getCallFromBuddy(bud)
+			try:
+				if c and c.connected:
+					c.sendInstantMessage(send_im_param)
+				else:
+					bud.sendInstantMessage(send_im_param)
+			except:
+				# error will be handled via Account::onInstantMessageStatus()
+				pass
+
 	def isPrivate(self):
 		return len(self._participantList) <= 1
 		
-	def isBuddyParticipant(self, bud, call_inst):
+	def isBuddyParticipant(self, bud):
 		return bud in self._participantList
 		
@@ -79,16 +114,30 @@
 		try:
 			idx = self._participantList.index(bud)
-			print "registering call...", idx, self._participantList
 			if len(self._callList) < idx+1:
 				self._callList.append(call_inst)
 			else:
 				self._callList[idx] = call_inst
+
+			call_inst.chat = self
+			call_inst.peerUri = bud.cfg.uri
 		except:
-			return None
+			pass
 		
 	def showWindow(self):
 		self._gui.bringToFront()
 
+	# helper
+	def dumpParticipantList(self):
+		print "Number of participants: %d" % (len(self._participantList))
+		for b in self._participantList:
+			print b.cfg.uri
+		
 	def addParticipant(self, bud, call_inst=None):
+		# avoid duplication
+		if self.isBuddyParticipant(bud): return
+		for b in self._participantList:
+			if bud.cfg.uri == b.cfg.uri: return
+			
+		# add it
 		self._participantList.append(bud)
 		if call_inst:
@@ -97,9 +146,6 @@
 
 		self._updateGui()
-		if not self.isPrivate():
-			self.startCall()
-			self._gui.enableAudio()
-	
-	def kickParticipant(bud):
+	
+	def kickParticipant(self, bud):
 		if bud in self._participantList:
 			idx = self._participantList.index(bud)
@@ -113,6 +159,10 @@
 			self._updateGui()
 		else:
-			# will this eventually destroy itself?
+			# will remove entry from list eventually destroy this chat?
 			self._acc.chatList.remove(self)
+			
+			# let's destroy GUI manually
+			self._gui.destroy()
+			#self.destroy()
 			
 	def addMessage(self, from_uri, msg):
@@ -120,4 +170,5 @@
 			msg = from_uri + ': ' + msg
 			self._gui.textAddMessage(msg)
+			self._sendInstantMessage(msg, from_uri)
 		else:
 			self._gui.textAddMessage(msg, False)
@@ -127,4 +178,5 @@
 		
 	def startCall(self):
+		self._gui.enableAudio()
 		call_param = pj.CallOpParam()
 		call_param.opt.audioCount = 1
@@ -146,5 +198,7 @@
 				c.makeCall(bud.cfg.uri, call_param)
 			except:
-				self._gui.audioUpdateState(bud.cfg.uri, gui.AudioState.FAILED)
+				#self._callList[idx] = None
+				#self._gui.audioUpdateState(bud.cfg.uri, gui.AudioState.FAILED)
+				self.kickParticipant(bud)
 			
 	def stopCall(self):
@@ -155,9 +209,17 @@
 		del self._callList[:]
 		
-	def updateCallState(self, thecall, info):
-		if info.state == pj.PJSIP_INV_STATE_CONFIRMED:
+	def updateCallState(self, thecall, info = None):
+		# info is optional here, just to avoid calling getInfo() twice (in the caller and here)
+		if not info: info = thecall.getInfo()
+		if info.state < pj.PJSIP_INV_STATE_CONFIRMED:
+			self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.INITIALIZING)
+		elif info.state == pj.PJSIP_INV_STATE_CONFIRMED:
 			self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.CONNECTED)
 		elif info.state == pj.PJSIP_INV_STATE_DISCONNECTED:
-			self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED)
+			if info.lastStatusCode/100 != 2:
+				self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.FAILED)
+			else:
+				self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED)
+			
 			# reset entry in the callList
 			try:
@@ -166,4 +228,11 @@
 			except:
 				pass
+			
+			self.addMessage(None, "Call to '%s' disconnected: %s" % (thecall.peerUri, info.lastReason))
+			
+			# kick the disconnected participant, but the last (avoid zombie chat)
+			if not self.isPrivate():
+				bud = self._getBuddyFromUri(thecall.peerUri)
+				if bud: self.kickParticipant(bud)
 
 			
@@ -172,17 +241,5 @@
 	# Text
 	def onSendMessage(self, msg):
-		send_im_param = pj.SendInstantMessageParam()
-		send_im_param.content = str(msg)
-		for bud in self._participantList:
-			# send via call, if any, or buddy
-			c = self._getCallFromBuddy(bud)
-			try:
-				if c:
-					c.sendInstantMessage(send_im_param)
-				else:
-					bud.sendInstantMessage(send_im_param)
-			except:
-				# error will be handled via Account::onInstantMessageStatus()
-				pass
+		self._sendInstantMessage(msg)
 
 	def onStartTyping(self):
@@ -193,10 +250,22 @@
 		
 	# Audio
-	def onRetry(self, peer_uri):
-		pass
-	def onKick(self, peer_uri):
-		pass
+	def onHangup(self, peer_uri):
+		c = self._getCallFromUri(peer_uri, "onHangup()")
+		if not c: return
+		call_param = pj.CallOpParam()
+		c.hangup(call_param)
+
 	def onHold(self, peer_uri):
-		pass
+		c = self._getCallFromUri(peer_uri, "onHold()")
+		if not c: return
+		call_param = pj.CallOpParam()
+		c.setHold(call_param)
+
+	def onUnhold(self, peer_uri):
+		c = self._getCallFromUri(peer_uri, "onUnhold()")
+		if not c: return
+		call_param = pj.CallOpParam()
+		c.reinvite(call_param)
+		
 	def onRxMute(self, peer_uri, is_muted):
 		pass
@@ -208,6 +277,11 @@
 	# Chat room
 	def onAddParticipant(self):
-		#self.addParticipant()
-		pass
+		buds = []
+		dlg = AddParticipantDlg(None, self._app, buds)
+		if dlg.doModal():
+			for bud in buds:
+				self.addParticipant(bud)
+			self.startCall()
+				
 	def onStartAudio(self):
 		self.startCall()
@@ -215,2 +289,77 @@
 	def onStopAudio(self):
 		self.stopCall()
+
+
+class AddParticipantDlg(tk.Toplevel):
+	"""
+	List of buddies
+	"""
+	def __init__(self, parent, app, bud_list):
+		tk.Toplevel.__init__(self, parent)
+		self.title('Add participants..')
+		self.transient(parent)
+		self.parent = parent
+		self._app = app
+		self.buddyList = bud_list
+		
+		self.isOk = False
+		
+		self.createWidgets()
+	
+	def doModal(self):
+		if self.parent:
+			self.parent.wait_window(self)
+		else:
+			self.wait_window(self)
+		return self.isOk
+		
+	def createWidgets(self):
+		# buddy list
+		list_frame = ttk.Frame(self)
+		list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1, padx=20, pady=20)
+		#scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=list_frame.yview)
+		#list_frame.config(yscrollcommand=scrl.set)
+		#scrl.pack(side=tk.RIGHT, fill=tk.Y)
+		
+		# draw buddy list
+		self.buddies = []
+		for acc in self._app.accList:
+			self.buddies.append((0, acc.cfg.idUri))
+			for bud in acc.buddyList:
+				self.buddies.append((1, bud))
+		
+		self.bud_var = []
+		for idx,(flag,bud) in enumerate(self.buddies):
+			self.bud_var.append(tk.IntVar())
+			if flag==0:
+				s = ttk.Separator(list_frame, orient=tk.HORIZONTAL)
+				s.pack(fill=tk.X)
+				l = tk.Label(list_frame, anchor=tk.W, text="Account '%s':" % (bud))
+				l.pack(fill=tk.X)
+			else:
+				c = tk.Checkbutton(list_frame, anchor=tk.W, text=bud.cfg.uri, variable=self.bud_var[idx])
+				c.pack(fill=tk.X)
+		s = ttk.Separator(list_frame, orient=tk.HORIZONTAL)
+		s.pack(fill=tk.X)
+
+		# Ok/cancel buttons
+		tail_frame = ttk.Frame(self)
+		tail_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
+		
+		btnOk = ttk.Button(tail_frame, text='Ok', default=tk.ACTIVE, command=self.onOk)
+		btnOk.pack(side=tk.LEFT, padx=20, pady=10)
+		btnCancel = ttk.Button(tail_frame, text='Cancel', command=self.onCancel)
+		btnCancel.pack(side=tk.RIGHT, padx=20, pady=10)
+		
+	def onOk(self):
+		self.buddyList[:] = []
+		for idx,(flag,bud) in enumerate(self.buddies):
+			if not flag: continue
+			if self.bud_var[idx].get() and not (bud in self.buddyList):
+				self.buddyList.append(bud)
+			
+		self.isOk = True
+		self.destroy()
+		
+	def onCancel(self):
+		self.destroy()
Index: pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chatgui.py
===================================================================
--- pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chatgui.py	(revision 4675)
+++ pjproject/branches/projects/pjsua2/pjsip-apps/src/pygui/chatgui.py	(revision 4677)
@@ -74,5 +74,5 @@
 		self.columnconfigure(1, weight=0)
 		
-		self._text = tk.Text(self, font=("Arial", "10"))
+		self._text = tk.Text(self, width=50, height=30, font=("Arial", "10"))
 		self._text.grid(row=0, column=0, sticky='nswe')
 		self._text.config(state=tk.DISABLED)
@@ -83,5 +83,5 @@
 		scrl.grid(row=0, column=1, sticky='nsw')
 		
-		self._typingBox = tk.Text(self, height=1, font=("Arial", "10"))
+		self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10"))
 		self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0)
 		
@@ -112,9 +112,9 @@
 			
 class AudioObserver:
-	def onRetry(self, peer_uri):
-		pass
-	def onKick(self, peer_uri):
+	def onHangup(self, peer_uri):
 		pass
 	def onHold(self, peer_uri):
+		pass
+	def onUnhold(self, peer_uri):
 		pass
 	def onRxMute(self, peer_uri, is_muted):
@@ -135,6 +135,4 @@
 		self._rxMute = False
 		self._txMute = False
-
-		# internal state: 0:init - 1:established - 2:failure/rejected 3:normal cleared
 		self._state = AudioState.NULL
 		
@@ -149,5 +147,4 @@
 			self._callFrame.pack_forget()
 			self._initFrame.pack(fill=tk.BOTH)
-			self._btnRetry.pack_forget()
 			self._btnCancel.pack(side=tk.TOP)
 			self._lblInitState['text'] = 'Intializing..'
@@ -161,29 +158,28 @@
 			if state == AudioState.FAILED:
 				self._lblInitState['text'] = 'Failed'
-				self._btnRetry.pack()
 			else:
 				self._lblInitState['text'] = 'Normal cleared'
 				self._btnCancel.pack_forget()
+			
+			self._btnHold['text'] = 'Hold'
+			self._btnHold.config(state=tk.NORMAL)
 		
 		# save last state
 		self._state = state
 		
-	def _onRetry(self):
-		self._btnRetry.pack_forget()
-		self._state = 0
-		# notify app
-		self._observer.onRetry(self.peerUri)
-
-	def _onKick(self):
-		# notify app
-		self._observer.onKick(self.peerUri)
-		
 	def _onHold(self):
-		# notify app
-		self._observer.onHold(self.peerUri)
+		self._btnHold.config(state=tk.DISABLED)
+		# notify app
+		if self._btnHold['text'] == 'Hold':
+			self._observer.onHold(self.peerUri)
+			self._btnHold['text'] = 'Unhold'
+		else:
+			self._observer.onUnhold(self.peerUri)
+			self._btnHold['text'] = 'Hold'
+		self._btnHold.config(state=tk.NORMAL)
 
 	def _onHangup(self):
 		# notify app
-		self._observer.onKick(self.peerUri)
+		self._observer.onHangup(self.peerUri)
 
 	def _onRxMute(self):
@@ -210,8 +206,6 @@
 		self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1)
 		
-		# Operation: retry, cancel/kick
-		self._btnRetry = ttk.Button(self._initFrame, text = 'Retry', command=self._onRetry)
-		self._btnRetry.pack(side=tk.TOP)
-		self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onKick)
+		# Operation: cancel/kick
+		self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup)
 		self._btnCancel.pack(side=tk.TOP)
 				
@@ -383,4 +377,8 @@
 				aud_frm.updateState(state)
 				break
+		if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1:
+			self.enableAudio(False)
+		else:
+			self.enableAudio(True)
 						
 if __name__ == '__main__':
