1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "GUIDialogMediaSource.h"
10 #include "ServiceBroker.h"
11 #include "guilib/GUIKeyboardFactory.h"
12 #include "GUIDialogFileBrowser.h"
13 #include "video/windows/GUIWindowVideoBase.h"
14 #include "music/windows/GUIWindowMusicBase.h"
15 #include "guilib/GUIComponent.h"
16 #include "guilib/GUIWindowManager.h"
17 #include "input/Key.h"
18 #include "Util.h"
19 #include "utils/URIUtils.h"
20 #include "utils/StringUtils.h"
21 #include "utils/Variant.h"
22 #include "filesystem/Directory.h"
23 #include "filesystem/PVRDirectory.h"
24 #include "GUIDialogYesNo.h"
25 #include "FileItem.h"
26 #include "settings/MediaSourceSettings.h"
27 #include "settings/Settings.h"
28 #include "settings/SettingsComponent.h"
29 #include "guilib/LocalizeStrings.h"
30 #include "PasswordManager.h"
31 #include "URL.h"
32 #include "pvr/recordings/PVRRecordingsPath.h"
33 
34 #if defined(TARGET_ANDROID)
35 #include "platform/android/activity/XBMCApp.h"
36 #include "filesystem/File.h"
37 #endif
38 
39 #ifdef TARGET_WINDOWS_STORE
40 #include "platform/win10/filesystem/WinLibraryDirectory.h"
41 #endif
42 
43 using namespace XFILE;
44 
45 #define CONTROL_HEADING          2
46 #define CONTROL_PATH            10
47 #define CONTROL_PATH_BROWSE     11
48 #define CONTROL_NAME            12
49 #define CONTROL_PATH_ADD        13
50 #define CONTROL_PATH_REMOVE     14
51 #define CONTROL_OK              18
52 #define CONTROL_CANCEL          19
53 #define CONTROL_CONTENT         20
54 
CGUIDialogMediaSource(void)55 CGUIDialogMediaSource::CGUIDialogMediaSource(void)
56     : CGUIDialog(WINDOW_DIALOG_MEDIA_SOURCE, "DialogMediaSource.xml")
57 {
58   m_paths = new CFileItemList;
59   m_loadType = KEEP_IN_MEMORY;
60 }
61 
~CGUIDialogMediaSource()62 CGUIDialogMediaSource::~CGUIDialogMediaSource()
63 {
64   delete m_paths;
65 }
66 
OnBack(int actionID)67 bool CGUIDialogMediaSource::OnBack(int actionID)
68 {
69   m_confirmed = false;
70   return CGUIDialog::OnBack(actionID);
71 }
72 
OnMessage(CGUIMessage & message)73 bool CGUIDialogMediaSource::OnMessage(CGUIMessage& message)
74 {
75   switch (message.GetMessage())
76   {
77   case GUI_MSG_CLICKED:
78   {
79     int iControl = message.GetSenderId();
80     int iAction = message.GetParam1();
81     if (iControl == CONTROL_PATH && (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK))
82       OnPath(GetSelectedItem());
83     else if (iControl == CONTROL_PATH_BROWSE)
84       OnPathBrowse(GetSelectedItem());
85     else if (iControl == CONTROL_PATH_ADD)
86       OnPathAdd();
87     else if (iControl == CONTROL_PATH_REMOVE)
88       OnPathRemove(GetSelectedItem());
89     else if (iControl == CONTROL_NAME)
90     {
91       OnEditChanged(iControl, m_name);
92       UpdateButtons();
93     }
94     else if (iControl == CONTROL_OK)
95       OnOK();
96     else if (iControl == CONTROL_CANCEL)
97       OnCancel();
98     else
99       break;
100     return true;
101   }
102   break;
103   case GUI_MSG_WINDOW_INIT:
104   {
105     UpdateButtons();
106   }
107   break;
108   case GUI_MSG_SETFOCUS:
109     if (message.GetControlId() == CONTROL_PATH_BROWSE ||
110       message.GetControlId() == CONTROL_PATH_ADD ||
111       message.GetControlId() == CONTROL_PATH_REMOVE)
112     {
113       HighlightItem(GetSelectedItem());
114     }
115     else
116       HighlightItem(-1);
117     break;
118   }
119   return CGUIDialog::OnMessage(message);
120 }
121 
122 // \brief Show CGUIDialogMediaSource dialog and prompt for a new media source.
123 // \return True if the media source is added, false otherwise.
ShowAndAddMediaSource(const std::string & type)124 bool CGUIDialogMediaSource::ShowAndAddMediaSource(const std::string &type)
125 {
126   CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
127   if (!dialog) return false;
128   dialog->Initialize();
129   dialog->SetShare(CMediaSource());
130   dialog->SetTypeOfMedia(type);
131   dialog->Open();
132   bool confirmed(dialog->IsConfirmed());
133   if (confirmed)
134   {
135     // Add this media source
136     // Get unique source name
137     std::string strName = dialog->GetUniqueMediaSourceName();
138 
139     CMediaSource share;
140     share.FromNameAndPaths(type, strName, dialog->GetPaths());
141     if (dialog->m_paths->Size() > 0)
142       share.m_strThumbnailImage = dialog->m_paths->Get(0)->GetArt("thumb");
143     CMediaSourceSettings::GetInstance().AddShare(type, share);
144     OnMediaSourceChanged(type, "", share);
145   }
146   dialog->m_paths->Clear();
147   return confirmed;
148 }
149 
ShowAndEditMediaSource(const std::string & type,const std::string & share)150 bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const std::string&share)
151 {
152   VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(type);
153   if (pShares)
154   {
155     for (unsigned int i = 0;i<pShares->size();++i)
156     {
157       if (StringUtils::EqualsNoCase((*pShares)[i].strName, share))
158         return ShowAndEditMediaSource(type, (*pShares)[i]);
159     }
160   }
161   return false;
162 }
163 
ShowAndEditMediaSource(const std::string & type,const CMediaSource & share)164 bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const CMediaSource &share)
165 {
166   std::string strOldName = share.strName;
167   CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
168   if (!dialog) return false;
169   dialog->Initialize();
170   dialog->SetShare(share);
171   dialog->SetTypeOfMedia(type, true);
172   dialog->Open();
173   bool confirmed(dialog->IsConfirmed());
174   if (confirmed)
175   {
176     // Update media source
177     // Get unique new source name when changed
178     std::string strName(dialog->m_name);
179     if (!StringUtils::EqualsNoCase(dialog->m_name, strOldName))
180       strName = dialog->GetUniqueMediaSourceName();
181 
182     CMediaSource newShare;
183     newShare.FromNameAndPaths(type, strName, dialog->GetPaths());
184     CMediaSourceSettings::GetInstance().UpdateShare(type, strOldName, newShare);
185 
186     OnMediaSourceChanged(type, strOldName, newShare);
187   }
188   dialog->m_paths->Clear();
189   return confirmed;
190 }
191 
GetUniqueMediaSourceName()192 std::string CGUIDialogMediaSource::GetUniqueMediaSourceName()
193 {
194   // Get unique source name for this media type
195   unsigned int i, j = 2;
196   bool bConfirmed = false;
197   VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(m_type);
198   std::string strName = m_name;
199   while (!bConfirmed)
200   {
201     for (i = 0; i<pShares->size(); ++i)
202     {
203       if (StringUtils::EqualsNoCase((*pShares)[i].strName, strName))
204         break;
205     }
206     if (i < pShares->size())
207       // found a match -  try next
208       strName = StringUtils::Format("%s (%i)", m_name.c_str(), j++);
209     else
210       bConfirmed = true;
211   }
212   return strName;
213 }
214 
OnMediaSourceChanged(const std::string & type,const std::string & oldName,const CMediaSource & share)215 void CGUIDialogMediaSource::OnMediaSourceChanged(const std::string& type, const std::string& oldName, const CMediaSource& share)
216 {
217   // Processing once media source added/edited - library scraping and scanning
218   if (!StringUtils::StartsWithNoCase(share.strPath, "rss://") &&
219     !StringUtils::StartsWithNoCase(share.strPath, "rsss://") &&
220     !StringUtils::StartsWithNoCase(share.strPath, "upnp://"))
221   {
222     if (type == "video" && !URIUtils::IsLiveTV(share.strPath))
223       // Assign content to a path, refresh scraper information optionally start a scan
224       CGUIWindowVideoBase::OnAssignContent(share.strPath);
225     else if (type == "music")
226       CGUIWindowMusicBase::OnAssignContent(oldName, share);
227   }
228 }
229 
OnPathBrowse(int item)230 void CGUIDialogMediaSource::OnPathBrowse(int item)
231 {
232   if (item < 0 || item >= m_paths->Size()) return;
233   // Browse is called.  Open the filebrowser dialog.
234   // Ignore current path is best at this stage??
235   std::string path = m_paths->Get(item)->GetPath();
236   bool allowNetworkShares(m_type != "programs");
237   VECSOURCES extraShares;
238 
239   if (m_name != CUtil::GetTitleFromPath(path))
240     m_bNameChanged = true;
241   path.clear();
242 
243   if (m_type == "music")
244   {
245     CMediaSource share1;
246 #if defined(TARGET_ANDROID)
247     // add the default android music directory
248     std::string path;
249     if (CXBMCApp::GetExternalStorage(path, "music") && !path.empty() && CDirectory::Exists(path))
250     {
251       share1.strPath = path;
252       share1.strName = g_localizeStrings.Get(20240);
253       share1.m_ignore = true;
254       extraShares.push_back(share1);
255     }
256 #endif
257 
258 #if defined(TARGET_WINDOWS_STORE)
259     // add the default UWP music directory
260     std::string path;
261     if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
262     {
263       share1.strPath = path;
264       share1.strName = g_localizeStrings.Get(20245);
265       share1.m_ignore = true;
266       extraShares.push_back(share1);
267     }
268 #endif
269 
270     // add the music playlist location
271     share1.strPath = "special://musicplaylists/";
272     share1.strName = g_localizeStrings.Get(20011);
273     share1.m_ignore = true;
274     extraShares.push_back(share1);
275 
276     // add the recordings dir as needed
277     if (CPVRDirectory::HasRadioRecordings())
278     {
279       share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS;
280       share1.strName = g_localizeStrings.Get(19017); // Recordings
281       extraShares.push_back(share1);
282     }
283     if (CPVRDirectory::HasDeletedRadioRecordings())
284     {
285       share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS;
286       share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
287       extraShares.push_back(share1);
288     }
289 
290     if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH) != "")
291     {
292       share1.strPath = "special://recordings/";
293       share1.strName = g_localizeStrings.Get(21883);
294       extraShares.push_back(share1);
295     }
296   }
297   else if (m_type == "video")
298   {
299     CMediaSource share1;
300 #if defined(TARGET_ANDROID)
301     // add the default android video directory
302     std::string path;
303     if (CXBMCApp::GetExternalStorage(path, "videos") && !path.empty() && CFile::Exists(path))
304     {
305       share1.strPath = path;
306       share1.strName = g_localizeStrings.Get(20241);
307       share1.m_ignore = true;
308       extraShares.push_back(share1);
309     }
310 #endif
311 #if defined(TARGET_WINDOWS_STORE)
312     // add the default UWP music directory
313     std::string path;
314     if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
315     {
316       share1.strPath = path;
317       share1.strName = g_localizeStrings.Get(20246);
318       share1.m_ignore = true;
319       extraShares.push_back(share1);
320     }
321 #endif
322 
323     // add the video playlist location
324     share1.m_ignore = true;
325     share1.strPath = "special://videoplaylists/";
326     share1.strName = g_localizeStrings.Get(20012);
327     extraShares.push_back(share1);
328 
329     // add the recordings dir as needed
330     if (CPVRDirectory::HasTVRecordings())
331     {
332       share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS;
333       share1.strName = g_localizeStrings.Get(19017); // Recordings
334       extraShares.push_back(share1);
335     }
336     if (CPVRDirectory::HasDeletedTVRecordings())
337     {
338       share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS;
339       share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
340       extraShares.push_back(share1);
341     }
342   }
343   else if (m_type == "pictures")
344   {
345     CMediaSource share1;
346 #if defined(TARGET_ANDROID)
347     // add the default android music directory
348     std::string path;
349     if (CXBMCApp::GetExternalStorage(path, "pictures") && !path.empty() && CFile::Exists(path))
350     {
351       share1.strPath = path;
352       share1.strName = g_localizeStrings.Get(20242);
353       share1.m_ignore = true;
354       extraShares.push_back(share1);
355     }
356 
357     path.clear();
358     if (CXBMCApp::GetExternalStorage(path, "photos") && !path.empty() && CFile::Exists(path))
359     {
360       share1.strPath = path;
361       share1.strName = g_localizeStrings.Get(20243);
362       share1.m_ignore = true;
363       extraShares.push_back(share1);
364     }
365 #endif
366 #if defined(TARGET_WINDOWS_STORE)
367     // add the default UWP music directory
368     std::string path;
369     if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
370     {
371       share1.strPath = path;
372       share1.strName = g_localizeStrings.Get(20247);
373       share1.m_ignore = true;
374       extraShares.push_back(share1);
375     }
376     path.clear();
377     if (XFILE::CWinLibraryDirectory::GetStoragePath("photos", path) && !path.empty() && CDirectory::Exists(path))
378     {
379       share1.strPath = path;
380       share1.strName = g_localizeStrings.Get(20248);
381       share1.m_ignore = true;
382       extraShares.push_back(share1);
383     }
384 #endif
385 
386     share1.m_ignore = true;
387     if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH) != "")
388     {
389       share1.strPath = "special://screenshots/";
390       share1.strName = g_localizeStrings.Get(20008);
391       extraShares.push_back(share1);
392     }
393   }
394   else if (m_type == "games")
395   {
396     // nothing to add
397   }
398   else if (m_type == "programs")
399   {
400     // nothing to add
401   }
402   if (CGUIDialogFileBrowser::ShowAndGetSource(path, allowNetworkShares, extraShares.size() == 0 ? NULL : &extraShares))
403   {
404     if (item < m_paths->Size()) // if the skin does funky things, m_paths may have been cleared
405       m_paths->Get(item)->SetPath(path);
406     if (!m_bNameChanged || m_name.empty())
407     {
408       CURL url(path);
409       m_name = url.GetWithoutUserDetails();
410       URIUtils::RemoveSlashAtEnd(m_name);
411       m_name = CUtil::GetTitleFromPath(m_name);
412     }
413     UpdateButtons();
414   }
415 }
416 
OnPath(int item)417 void CGUIDialogMediaSource::OnPath(int item)
418 {
419   if (item < 0 || item >= m_paths->Size()) return;
420 
421   std::string path(m_paths->Get(item)->GetPath());
422   if (m_name != CUtil::GetTitleFromPath(path))
423     m_bNameChanged = true;
424 
425   CGUIKeyboardFactory::ShowAndGetInput(path, CVariant{ g_localizeStrings.Get(1021) }, false);
426   m_paths->Get(item)->SetPath(path);
427 
428   if (!m_bNameChanged || m_name.empty())
429   {
430     CURL url(m_paths->Get(item)->GetPath());
431     m_name = url.GetWithoutUserDetails();
432     URIUtils::RemoveSlashAtEnd(m_name);
433     m_name = CUtil::GetTitleFromPath(m_name);
434   }
435   UpdateButtons();
436 }
437 
OnOK()438 void CGUIDialogMediaSource::OnOK()
439 {
440   // Verify the paths by doing a GetDirectory.
441   CFileItemList items;
442 
443   // Create temp media source to encode path urls as multipath
444   // Name of actual source may need to be made unique when saved in sources
445   CMediaSource share;
446   share.FromNameAndPaths(m_type, m_name, GetPaths());
447 
448   if (StringUtils::StartsWithNoCase(share.strPath, "plugin://") ||
449     CDirectory::GetDirectory(share.strPath, items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_ALLOW_PROMPT) ||
450     CGUIDialogYesNo::ShowAndGetInput(CVariant{ 1001 }, CVariant{ 1025 }))
451   {
452     m_confirmed = true;
453     Close();
454   }
455 }
456 
OnCancel()457 void CGUIDialogMediaSource::OnCancel()
458 {
459   m_confirmed = false;
460   Close();
461 }
462 
UpdateButtons()463 void CGUIDialogMediaSource::UpdateButtons()
464 {
465   if (!m_paths->Size()) // sanity
466     return;
467 
468   CONTROL_ENABLE_ON_CONDITION(CONTROL_OK, !m_paths->Get(0)->GetPath().empty() && !m_name.empty());
469   CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_ADD, !m_paths->Get(0)->GetPath().empty());
470   CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_REMOVE, m_paths->Size() > 1);
471   // name
472   SET_CONTROL_LABEL2(CONTROL_NAME, m_name);
473   SendMessage(GUI_MSG_SET_TYPE, CONTROL_NAME, 0, 1022);
474 
475   int currentItem = GetSelectedItem();
476   SendMessage(GUI_MSG_LABEL_RESET, CONTROL_PATH);
477   for (int i = 0; i < m_paths->Size(); i++)
478   {
479     CFileItemPtr item = m_paths->Get(i);
480     std::string path;
481     CURL url(item->GetPath());
482     path = url.GetWithoutUserDetails();
483     if (path.empty()) path = "<" + g_localizeStrings.Get(231) + ">"; // <None>
484     item->SetLabel(path);
485   }
486   CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PATH, 0, 0, m_paths);
487   OnMessage(msg);
488   SendMessage(GUI_MSG_ITEM_SELECT, CONTROL_PATH, currentItem);
489 
490   SET_CONTROL_HIDDEN(CONTROL_CONTENT);
491 }
492 
SetShare(const CMediaSource & share)493 void CGUIDialogMediaSource::SetShare(const CMediaSource &share)
494 {
495   m_paths->Clear();
496   for (unsigned int i = 0; i < share.vecPaths.size(); i++)
497   {
498     CFileItemPtr item(new CFileItem(share.vecPaths[i], true));
499     m_paths->Add(item);
500   }
501   if (0 == share.vecPaths.size())
502   {
503     CFileItemPtr item(new CFileItem("", true));
504     m_paths->Add(item);
505   }
506   m_name = share.strName;
507   UpdateButtons();
508 }
509 
SetTypeOfMedia(const std::string & type,bool editNotAdd)510 void CGUIDialogMediaSource::SetTypeOfMedia(const std::string &type, bool editNotAdd)
511 {
512   m_type = type;
513   std::string heading;
514   if (editNotAdd)
515   {
516     if (type == "video")
517       heading = g_localizeStrings.Get(10053);
518     else if (type == "music")
519       heading = g_localizeStrings.Get(10054);
520     else if (type == "pictures")
521       heading = g_localizeStrings.Get(10055);
522     else if (type == "games")
523       heading = g_localizeStrings.Get(35252); // "Edit game source"
524     else if (type == "programs")
525       heading = g_localizeStrings.Get(10056);
526     else
527       heading = g_localizeStrings.Get(10057);
528   }
529   else
530   {
531     if (type == "video")
532       heading = g_localizeStrings.Get(10048);
533     else if (type == "music")
534       heading = g_localizeStrings.Get(10049);
535     else if (type == "pictures")
536       heading = g_localizeStrings.Get(13006);
537     else if (type == "games")
538       heading = g_localizeStrings.Get(35251); // "Add game source"
539     else if (type == "programs")
540       heading = g_localizeStrings.Get(10051);
541     else
542       heading = g_localizeStrings.Get(10052);
543   }
544   SET_CONTROL_LABEL(CONTROL_HEADING, heading);
545 }
546 
GetSelectedItem()547 int CGUIDialogMediaSource::GetSelectedItem()
548 {
549   CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PATH);
550   OnMessage(message);
551   int value = message.GetParam1();
552   if (value < 0 || value >= m_paths->Size()) return 0;
553   return value;
554 }
555 
HighlightItem(int item)556 void CGUIDialogMediaSource::HighlightItem(int item)
557 {
558   for (int i = 0; i < m_paths->Size(); i++)
559     m_paths->Get(i)->Select(false);
560   if (item >= 0 && item < m_paths->Size())
561     m_paths->Get(item)->Select(true);
562   CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PATH, item);
563   OnMessage(msg);
564 }
565 
OnPathRemove(int item)566 void CGUIDialogMediaSource::OnPathRemove(int item)
567 {
568   m_paths->Remove(item);
569   UpdateButtons();
570   if (item >= m_paths->Size())
571     HighlightItem(m_paths->Size() - 1);
572   else
573     HighlightItem(item);
574   if (m_paths->Size() <= 1)
575   {
576     SET_CONTROL_FOCUS(CONTROL_PATH_ADD, 0);
577   }
578 }
579 
OnPathAdd()580 void CGUIDialogMediaSource::OnPathAdd()
581 {
582   // add a new item and select it as well
583   CFileItemPtr item(new CFileItem("", true));
584   m_paths->Add(item);
585   UpdateButtons();
586   HighlightItem(m_paths->Size() - 1);
587 }
588 
GetPaths() const589 std::vector<std::string> CGUIDialogMediaSource::GetPaths() const
590 {
591   std::vector<std::string> paths;
592   for (int i = 0; i < m_paths->Size(); i++)
593   {
594     if (!m_paths->Get(i)->GetPath().empty())
595     { // strip off the user and password for supported paths (anything that the password manager can auth)
596       // and add the user/pass to the password manager - note, we haven't confirmed that it works
597       // at this point, but if it doesn't, the user will get prompted anyway in implementation.
598       CURL url(m_paths->Get(i)->GetPath());
599       if (CPasswordManager::GetInstance().IsURLSupported(url) && !url.GetUserName().empty())
600       {
601         CPasswordManager::GetInstance().SaveAuthenticatedURL(url);
602         url.SetPassword("");
603         url.SetUserName("");
604         url.SetDomain("");
605       }
606       paths.push_back(url.Get());
607     }
608   }
609   return paths;
610 }
611 
OnDeinitWindow(int nextWindowID)612 void CGUIDialogMediaSource::OnDeinitWindow(int nextWindowID)
613 {
614   CGUIDialog::OnDeinitWindow(nextWindowID);
615 
616   // clear paths container
617   CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PATH, 0);
618   OnMessage(msg);
619 }
620