|
1 /* |
|
2 * Copyright (c) 2005 Nokia Corporation and/or its subsidiary(-ies). |
|
3 * All rights reserved. |
|
4 * This component and the accompanying materials are made available |
|
5 * under the terms of "Eclipse Public License v1.0" |
|
6 * which accompanies this distribution, and is available |
|
7 * at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
8 * |
|
9 * Initial Contributors: |
|
10 * Nokia Corporation - initial contribution. |
|
11 * |
|
12 * Contributors: |
|
13 * |
|
14 * Description: Fota download client |
|
15 * |
|
16 */ |
|
17 #include <XQConversions> |
|
18 #include "FotaDlClient.h" |
|
19 #include "FotaDlMgrClient.h" |
|
20 |
|
21 // ----------------------------------------------------------------------------- |
|
22 // The constructor of this class |
|
23 // ----------------------------------------------------------------------------- |
|
24 DownloadClient::DownloadClient(DownloadManagerClient* Observer) |
|
25 { |
|
26 FLOG(_L("DownloadClient::DownloadClient >>")); |
|
27 |
|
28 iFotaDlMgrClient = Observer; |
|
29 iTotalSize = -1; |
|
30 iSpaceChecked = 0; |
|
31 iDownload = NULL; |
|
32 iClientinterrupted = false; |
|
33 iClientError = ErrorNone; |
|
34 iContentType = TUnknownType; |
|
35 |
|
36 FLOG(_L("DownloadClient::DownloadClient <<")); |
|
37 } |
|
38 |
|
39 // ----------------------------------------------------------------------------- |
|
40 // The destructor of this class |
|
41 // ----------------------------------------------------------------------------- |
|
42 DownloadClient::~DownloadClient() |
|
43 { |
|
44 FLOG(_L("DownloadClient::~DownloadClient >>")); |
|
45 |
|
46 if (iDownload) |
|
47 { |
|
48 disconnect(iDownload, SIGNAL(downloadEvent(DownloadEvent *)), this, |
|
49 SLOT(DownloadEventRecieved(DownloadEvent *))); |
|
50 |
|
51 //Do not delete iDownload as it is owned by download manager. |
|
52 //delete iDownload; |
|
53 iDownload = NULL; |
|
54 } |
|
55 //Don't delete as it is not owned |
|
56 iFotaDlMgrClient = NULL; |
|
57 |
|
58 FLOG(_L("DownloadClient::~DownloadClient <<")); |
|
59 } |
|
60 |
|
61 // ----------------------------------------------------------------------------- |
|
62 // Creates a single download with the download manager |
|
63 // ----------------------------------------------------------------------------- |
|
64 TInt DownloadClient::CreateDownload(const QString &url, DownloadType type) |
|
65 { |
|
66 FLOG(_L("DownloadClient::CreateDownload >>")); |
|
67 |
|
68 TInt ret = ENotOk; |
|
69 |
|
70 iDownload = iFotaDlMgrClient->Manager()->createDownload(url, type); //Step 6 |
|
71 |
|
72 if (iDownload) |
|
73 { |
|
74 connect(iDownload, SIGNAL(downloadEvent(DownloadEvent *)), this, |
|
75 SLOT(DownloadEventRecieved(DownloadEvent *))); |
|
76 ret = EOk; |
|
77 } |
|
78 else |
|
79 { |
|
80 FLOG(_L("Download creation is unsuccessful!")); |
|
81 } |
|
82 |
|
83 FLOG(_L("DownloadClient::CreateDownload, ret = %d <<"), ret); |
|
84 return ret; |
|
85 } |
|
86 |
|
87 // ----------------------------------------------------------------------------- |
|
88 // Sets the required attributes for the single download. |
|
89 // ----------------------------------------------------------------------------- |
|
90 TInt DownloadClient::SetDownloadAttributes() |
|
91 { |
|
92 FLOG(_L("DownloadClient::SetDownloadAttributes >>")); |
|
93 |
|
94 int ret(EOk); |
|
95 |
|
96 iDownload->setAttribute(ProgressInterval, 100); //Step 7 |
|
97 iDownload->setAttribute(Priority, High); //Step 8 |
|
98 |
|
99 //Setting the default path |
|
100 iDownload->setAttribute(DestinationPath, DefaultPath); |
|
101 |
|
102 iDownload->setAttribute(FileName, PackageName); |
|
103 |
|
104 FLOG(_L("DownloadClient::SetDownloadAttributes, ret = %d <<"), ret); |
|
105 |
|
106 return ret; |
|
107 } |
|
108 |
|
109 // ----------------------------------------------------------------------------- |
|
110 // Starts the single download. Download should be created and attributes set before this. |
|
111 // ----------------------------------------------------------------------------- |
|
112 TInt DownloadClient::Start() |
|
113 { |
|
114 FLOG(_L("DownloadClient::Start >>")); |
|
115 |
|
116 iClientinterrupted = false; |
|
117 iDownload->start(); //Step 9 |
|
118 |
|
119 FLOG(_L("DownloadClient::Start <<")); |
|
120 return EOk; |
|
121 |
|
122 } |
|
123 |
|
124 // ----------------------------------------------------------------------------- |
|
125 // Gets the attribute of the single download |
|
126 // ----------------------------------------------------------------------------- |
|
127 inline QVariant DownloadClient::GetDownloadAttribute(DownloadAttribute attr) |
|
128 { |
|
129 FLOG(_L("DownloadClient::GetDownloadAttribute >>")); |
|
130 |
|
131 QVariant val; |
|
132 |
|
133 val = iDownload->attribute(attr); |
|
134 |
|
135 FLOG(_L("DownloadClient::GetDownloadAttribute<<")); |
|
136 return val; |
|
137 } |
|
138 |
|
139 // ----------------------------------------------------------------------------- |
|
140 // Pauses the single download. |
|
141 // ----------------------------------------------------------------------------- |
|
142 TInt DownloadClient::Pause(TClientErrorType aReason) |
|
143 { |
|
144 FLOG(_L("DownloadClient::Pause >>")); |
|
145 |
|
146 iClientinterrupted = true; |
|
147 iClientError = aReason; |
|
148 |
|
149 iDownload->pause(); |
|
150 |
|
151 FLOG(_L("DownloadClient::Pause <<")); |
|
152 return EOk; |
|
153 } |
|
154 |
|
155 // ----------------------------------------------------------------------------- |
|
156 // Resumes the single download. |
|
157 // ----------------------------------------------------------------------------- |
|
158 TInt DownloadClient::Resume() |
|
159 { |
|
160 FLOG(_L("DownloadClient::Resume >>")); |
|
161 |
|
162 int ret(ENotOk); |
|
163 iSpaceChecked = true; |
|
164 |
|
165 if (!iDownload) |
|
166 { |
|
167 QList<Download*> dls = |
|
168 iFotaDlMgrClient->Manager()->currentDownloads(); |
|
169 int count = dls.count(); |
|
170 FLOG(_L("Number of current downloads = %d"), count); |
|
171 |
|
172 if (count) |
|
173 { |
|
174 iDownload = dls[0]; |
|
175 FLOG(_L("Connecting to download event")); |
|
176 connect(iDownload, SIGNAL(downloadEvent(DownloadEvent *)), this, |
|
177 SLOT(DownloadEventRecieved(DownloadEvent *))); |
|
178 FLOG(_L("Connecting to download events done")); |
|
179 } |
|
180 |
|
181 } |
|
182 iClientinterrupted = false; |
|
183 iClientError = ErrorNone; |
|
184 |
|
185 if (iDownload) |
|
186 { |
|
187 FLOG(_L("B4 Download Start")); |
|
188 iDownload->start(); |
|
189 ret = EOk; |
|
190 FLOG(_L("After Download Start")); |
|
191 } |
|
192 |
|
193 FLOG(_L("DownloadClient::Resume, ret = %d <<"), ret); |
|
194 return ret; |
|
195 } |
|
196 |
|
197 // ----------------------------------------------------------------------------- |
|
198 // Cancels the single download. |
|
199 // ----------------------------------------------------------------------------- |
|
200 TInt DownloadClient::Cancel(TClientErrorType aReason) |
|
201 { |
|
202 FLOG(_L("DownloadClient::Cancel >>")); |
|
203 |
|
204 iClientinterrupted = true; |
|
205 iClientError = aReason; |
|
206 |
|
207 iDownload->cancel(); |
|
208 |
|
209 FLOG(_L("DownloadClient::Cancel <<")); |
|
210 return EOk; |
|
211 } |
|
212 |
|
213 // ----------------------------------------------------------------------------- |
|
214 // Restarts the single download. This is equivalent to cancel and start on Download. |
|
215 // ----------------------------------------------------------------------------- |
|
216 TInt DownloadClient::Restart() |
|
217 { |
|
218 FLOG(_L("DownloadClient::Restart >>")); |
|
219 |
|
220 QString path(NULL); |
|
221 int size = iTotalSize; |
|
222 TFreeSpace avail = iFotaDlMgrClient->GetSuitablePath(size, path); |
|
223 |
|
224 if (avail == EFitsToReservation || avail == EFitsToFileSystem) |
|
225 { |
|
226 //FLOG(_L("Space is available for download at %S"), TPtrC (reinterpret_cast<const TText*> (path.constData()),path.length())); |
|
227 |
|
228 if (iContentType == TOmaDL10Download) |
|
229 { |
|
230 ReadDescriptorData(); |
|
231 } |
|
232 Resume(); |
|
233 } |
|
234 else |
|
235 { |
|
236 FLOG(_L("Space not available. Download is stopped!")); |
|
237 TRAP_IGNORE(iFotaDlMgrClient->ShowDialogL(EFwDLNeedMoreMemory, size)); |
|
238 } |
|
239 |
|
240 FLOG(_L("DownloadClient::Restart >>")); |
|
241 |
|
242 return EOk; |
|
243 } |
|
244 |
|
245 // ----------------------------------------------------------------------------- |
|
246 // The slot which receives all the single download events. |
|
247 // ----------------------------------------------------------------------------- |
|
248 bool DownloadClient::DownloadEventRecieved(DownloadEvent *event) |
|
249 { |
|
250 FLOG(_L("DownloadClient::event >>")); |
|
251 |
|
252 DownloadEvent::Event type = (DownloadEvent::Event) event->type(); |
|
253 bool eventHandled = false; |
|
254 int err0(NoError); |
|
255 |
|
256 FLOG(_L("Download Event Type: %d"), type); |
|
257 |
|
258 switch (type) |
|
259 { |
|
260 case DownloadEvent::Started: |
|
261 { |
|
262 FLOG(_L("DownloadEventRecieved - DownloadEvent::Started")); |
|
263 iProgress = true; |
|
264 iDlState = Download::Created; |
|
265 |
|
266 eventHandled = true; |
|
267 break; |
|
268 } |
|
269 |
|
270 case DownloadEvent::HeadersReceived: |
|
271 { |
|
272 FLOG(_L("DownloadEventRecieved - DownloadEvent::HeadersReceived")); |
|
273 iProgress = true; |
|
274 iDlState = Download::Created; |
|
275 |
|
276 QString contenttype = |
|
277 GetDownloadAttribute(ContentType).toString(); |
|
278 HBufC* s_contenttype = XQConversions::qStringToS60Desc(contenttype); |
|
279 FLOG(_L("Content type received is %S"), s_contenttype); |
|
280 delete s_contenttype; |
|
281 |
|
282 iContentType = CheckContentType(contenttype); |
|
283 |
|
284 if (iContentType == THttpDownload) |
|
285 { |
|
286 FLOG(_L("Content type: Http; checking the size of download")); |
|
287 if (!iSpaceChecked) |
|
288 { |
|
289 |
|
290 iTotalSize = GetDownloadAttribute(TotalSize).toInt(); |
|
291 |
|
292 FLOG(_L("Size of the firmware update as received is %d"), |
|
293 iTotalSize); |
|
294 |
|
295 if (iTotalSize > 0) |
|
296 { |
|
297 QString path(NULL); |
|
298 |
|
299 TFreeSpace avail = iFotaDlMgrClient->GetSuitablePath( |
|
300 iTotalSize, path); |
|
301 |
|
302 if (avail == EFitsToReservation || avail |
|
303 == EFitsToFileSystem) |
|
304 { |
|
305 //FLOG(_L("Space is available for download at %S"),TPtrC (reinterpret_cast<const TText*> (path.constData()),path.length())); |
|
306 iFotaDlMgrClient->DeleteDiskReservation(path); |
|
307 iDownload->setAttribute(DestinationPath, path); |
|
308 } |
|
309 else |
|
310 { |
|
311 FLOG( |
|
312 _L("Space not available. Download is stopped!")); |
|
313 Pause(NeedMoreMemory); |
|
314 TRAP_IGNORE(iFotaDlMgrClient->ShowDialogL(EFwDLNeedMoreMemory)); |
|
315 break; |
|
316 } |
|
317 |
|
318 iSpaceChecked = true; |
|
319 iFotaDlMgrClient->StartDownloadProgress( |
|
320 QString::null, QString::null, iTotalSize); |
|
321 } |
|
322 } |
|
323 } |
|
324 else if (iContentType == TUnknownType) |
|
325 { |
|
326 FLOG(_L("Content type unknown; hence cancelling download !")); |
|
327 Cancel(InvalidContentType); |
|
328 } |
|
329 |
|
330 eventHandled = true; |
|
331 break; |
|
332 } |
|
333 |
|
334 case DownloadEvent::InProgress: |
|
335 { |
|
336 FLOG(_L("DownloadEventRecieved - DownloadEvent::InProgress")); |
|
337 |
|
338 iProgress = true; |
|
339 iDlState = Download::InProgress; |
|
340 UpdateDownloadProgress(); |
|
341 |
|
342 eventHandled = true; |
|
343 break; |
|
344 } |
|
345 |
|
346 case DownloadEvent::Completed: |
|
347 { |
|
348 FLOG(_L("DownloadEventRecieved - DownloadEvent::Completed")); |
|
349 iProgress = false; |
|
350 iDlState = Download::Completed; |
|
351 iTotalSize = 0; |
|
352 UpdateDownloadProgress(); |
|
353 |
|
354 eventHandled = true; |
|
355 break; |
|
356 } |
|
357 |
|
358 case DownloadEvent::Paused: |
|
359 { |
|
360 FLOG(_L("DownloadEventRecieved - DownloadEvent::Paused")); |
|
361 iProgress = false; |
|
362 iDlState = Download::Paused; |
|
363 |
|
364 eventHandled = true; |
|
365 break; |
|
366 } |
|
367 case DownloadEvent::Failed: |
|
368 { |
|
369 FLOG(_L("DownloadEventRecieved - DownloadEvent::Failed")); |
|
370 iProgress = false; |
|
371 iDlState = Download::Failed; |
|
372 |
|
373 eventHandled = true; |
|
374 break; |
|
375 } |
|
376 case DownloadEvent::Cancelled: |
|
377 { |
|
378 FLOG(_L("DownloadEventRecieved - DownloadEvent::Cancelled")); |
|
379 iProgress = false; |
|
380 iDlState = Download::Cancelled; |
|
381 |
|
382 eventHandled = true; |
|
383 break; |
|
384 } |
|
385 |
|
386 case DownloadEvent::NetworkLoss: |
|
387 { |
|
388 FLOG(_L("DownloadEventRecieved - DownloadEvent::NetworkLoss")); |
|
389 iProgress = false; |
|
390 iDlState = Download::Paused; |
|
391 |
|
392 eventHandled = true; |
|
393 break; |
|
394 } |
|
395 |
|
396 case DownloadEvent::Error: |
|
397 { |
|
398 FLOG(_L("DownloadEventRecieved - DownloadEvent::Error")); |
|
399 iProgress = false; |
|
400 // iDlState = (Download::State) GetDownloadAttribute(State).toInt(); |
|
401 iDlState = Download::Failed; |
|
402 eventHandled = true; |
|
403 break; |
|
404 } |
|
405 |
|
406 case DownloadEvent::DescriptorReady: |
|
407 { |
|
408 FLOG(_L("DownloadEventRecieved - DownloadEvent::DescriptorReady")); |
|
409 iProgress = true; |
|
410 iDlState = Download::InProgress; |
|
411 iContentType = TOmaDL10Download; |
|
412 |
|
413 iTotalSize = GetDownloadAttribute(DescriptorSize).toInt(); |
|
414 QString path(NULL); |
|
415 if (iTotalSize > 0) |
|
416 { |
|
417 |
|
418 TFreeSpace avail = iFotaDlMgrClient->GetSuitablePath( |
|
419 iTotalSize, path); |
|
420 |
|
421 if (avail == EFitsToReservation || avail == EFitsToFileSystem) |
|
422 { |
|
423 //FLOG(_L("Space is available for download at %S"), TPtrC (reinterpret_cast<const TText*> (path.constData()),path.length())); |
|
424 iFotaDlMgrClient->DeleteDiskReservation(path); |
|
425 } |
|
426 else |
|
427 { |
|
428 FLOG(_L("Space not available. Download is stopped!")); |
|
429 TRAP_IGNORE(iFotaDlMgrClient->ShowDialogL(EFwDLNeedMoreMemory)); |
|
430 break; |
|
431 } |
|
432 } |
|
433 ReadDescriptorData(); |
|
434 Resume(); |
|
435 SetSubDownloadAttributes(); |
|
436 |
|
437 eventHandled = true; |
|
438 break; |
|
439 } |
|
440 case DownloadEvent::CreatingConnection: |
|
441 { |
|
442 FLOG( |
|
443 _L("DownloadEventRecieved - DownloadEvent::CreatingConnection")); |
|
444 iProgress = true; |
|
445 iDlState = Download::InProgress; |
|
446 |
|
447 eventHandled = true; |
|
448 break; |
|
449 } |
|
450 case DownloadEvent::ConnectionNeeded: |
|
451 case DownloadEvent::ConnectionDisconnected: |
|
452 { |
|
453 FLOG( |
|
454 _L("DownloadEventRecieved - DownloadEvent::ConnectionNeeded/ConnectionDisconnected")); |
|
455 iProgress = false; |
|
456 iDlState = Download::Paused; |
|
457 err0 = ConnectionFailed; |
|
458 eventHandled = true; |
|
459 break; |
|
460 } |
|
461 |
|
462 default: |
|
463 { |
|
464 FLOG(_L("DownloadEventRecieved - Skipped this event: %d"), type); |
|
465 break; |
|
466 } |
|
467 } |
|
468 |
|
469 if (iProgress == false) |
|
470 { |
|
471 |
|
472 if (iClientinterrupted) |
|
473 { |
|
474 //Client has requested for cancellation. Hence provide the same error code |
|
475 err0 = iClientError; |
|
476 HandleClientInterrupt(iDlState, err0); |
|
477 } |
|
478 else |
|
479 { |
|
480 //Download Manager has cancelled download. Hence provide the last error code. |
|
481 if (err0 == NoError) |
|
482 err0 = GetDownloadAttribute(LastError).toInt(); |
|
483 |
|
484 HandleDownloadComplete(iDlState, err0); |
|
485 } |
|
486 |
|
487 } |
|
488 |
|
489 FLOG(_L("DownloadClient::event <<")); |
|
490 |
|
491 return eventHandled; |
|
492 } |
|
493 |
|
494 // ----------------------------------------------------------------------------- |
|
495 // Called to update the progress of download to fota server. This fetches the percentage |
|
496 // of download from download manager. |
|
497 // ----------------------------------------------------------------------------- |
|
498 inline void DownloadClient::UpdateDownloadProgress() |
|
499 { |
|
500 FLOG(_L("DownloadClient::UpdateDownloadProgress >>")); |
|
501 |
|
502 // Remaining size calculation |
|
503 int prog = GetDownloadAttribute(Percentage).toInt(); |
|
504 iFotaDlMgrClient->UpdateDownloadProgress(prog); |
|
505 |
|
506 FLOG(_L("DownloadClient::UpdateDownloadProgress, progress = %d <<"), prog); |
|
507 } |
|
508 |
|
509 // ----------------------------------------------------------------------------- |
|
510 // Called to read the OMA DL1.0 download descriptor. This will update the fota server with size and version. |
|
511 // ----------------------------------------------------------------------------- |
|
512 void DownloadClient::ReadDescriptorData() |
|
513 { |
|
514 FLOG(_L("DownloadClient::ReadDescriptorData >>")); |
|
515 |
|
516 QString name = GetDownloadAttribute(DescriptorName).toString(); // "name" in OMA dd |
|
517 QString version = GetDownloadAttribute(DescriptorVersion).toString(); // "version" in OMA dd |
|
518 QString type = GetDownloadAttribute(DescriptorType).toString(); // "type" in OMA dd |
|
519 int size = GetDownloadAttribute(DescriptorSize).toInt(); // "size" in OMA dd |
|
520 QString vendor = GetDownloadAttribute(DescriptorVendor).toString(); // "vendor" in OMA dd |
|
521 QString description = |
|
522 GetDownloadAttribute(DescriptorDescription).toString(); // "description" in OMA dd |
|
523 QString nxturl = GetDownloadAttribute(DescriptorNextURL).toString(); // "nextURL" in OMA dd |
|
524 |
|
525 TDownloadType gottype = CheckContentType(type); |
|
526 |
|
527 if (gottype == THttpDownload) |
|
528 { |
|
529 iFotaDlMgrClient->StartDownloadProgress(name, version, size); |
|
530 } |
|
531 else |
|
532 { |
|
533 FLOG( |
|
534 _L("The content type in descriptor is not appropriate! Hence cancelling download")); |
|
535 Cancel(InvalidContentType); |
|
536 } |
|
537 |
|
538 FLOG(_L("DownloadClient::ReadDescriptorData <<")); |
|
539 } |
|
540 |
|
541 // ----------------------------------------------------------------------------- |
|
542 // Called when download is complete, either successfully or unsuccessfully. |
|
543 // The arguments to this function is read to know the actual status. |
|
544 // ----------------------------------------------------------------------------- |
|
545 void DownloadClient::HandleDownloadComplete(Download::State dlstate, int err0) |
|
546 { |
|
547 FLOG(_L("DownloadClient::HandleDownloadComplete, idlstate = %d, err0 = %d>>"), dlstate, err0); |
|
548 |
|
549 iFotaDlMgrClient->HandleDownloadEvent(dlstate, err0); |
|
550 |
|
551 FLOG(_L("DownloadClient::HandleDownloadComplete <<")); |
|
552 } |
|
553 |
|
554 // ----------------------------------------------------------------------------- |
|
555 // Called to handle the post download interrupt operation when client cancels/pauses download. |
|
556 // ----------------------------------------------------------------------------- |
|
557 void DownloadClient::HandleClientInterrupt(Download::State dlstate, int err0) |
|
558 { |
|
559 FLOG(_L("DownloadClient::HandleClientInterrupt, idlstate = %d, err0 = %d >>"), dlstate, err0); |
|
560 |
|
561 iFotaDlMgrClient->HandleClientInterrupt(dlstate, err0); |
|
562 |
|
563 FLOG(_L("DownloadClient::HandleClientInterrupt <<")); |
|
564 } |
|
565 |
|
566 // ----------------------------------------------------------------------------- |
|
567 // Called to validate the content type of the download as received in header. |
|
568 // ----------------------------------------------------------------------------- |
|
569 TDownloadType DownloadClient::CheckContentType(const QString aContent) |
|
570 { |
|
571 FLOG(_L("DownloadClient::CheckContentType >>")); |
|
572 |
|
573 TDownloadType type(TUnknownType); |
|
574 |
|
575 QString semicollon(";"); |
|
576 QString contenttype = aContent; |
|
577 |
|
578 int index = aContent.indexOf(semicollon, 0); |
|
579 |
|
580 if (index > 0) |
|
581 { |
|
582 contenttype.chop(aContent.length() - index); |
|
583 } |
|
584 |
|
585 if (contenttype.compare(ContentTypeHttp) == 0) |
|
586 type = THttpDownload; |
|
587 else if (contenttype.compare(ContentTypeOmaDl) == 0) |
|
588 type = TOmaDL10Download; |
|
589 |
|
590 FLOG(_L("DownloadClient::CheckContentType, type = %d<<"), type); |
|
591 |
|
592 return type; |
|
593 } |
|
594 |
|
595 void DownloadClient::SetSubDownloadAttributes() |
|
596 { |
|
597 FLOG(_L("DownloadClient::SetSubDownloadAttributes >>")); |
|
598 |
|
599 QList<Download*> dls = iDownload->subDownloads(); |
|
600 |
|
601 if (dls.count() > 0) |
|
602 { |
|
603 Download* subdl = dls[0]; |
|
604 |
|
605 subdl->setAttribute(FileName, PackageName); |
|
606 } |
|
607 else |
|
608 { |
|
609 FLOG(_L("Error: There are no sub downloads!")); |
|
610 } |
|
611 |
|
612 FLOG(_L("DownloadClient::SetSubDownloadAttributes <<")); |
|
613 } |
|
614 |
|
615 //End of file |