upnpframework/upnpcommonui/src/upnplocalplayer.cpp
changeset 0 7f85d04be362
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/upnpframework/upnpcommonui/src/upnplocalplayer.cpp	Thu Dec 17 08:52:00 2009 +0200
@@ -0,0 +1,679 @@
+/*
+* Copyright (c) 2006-2007 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description:  Plays remote images and videos locally on the device
+*
+*/
+
+
+// INCLUDE FILES
+// System
+#include <AiwGenericParam.h>
+#include <AiwGenericParam.hrh>
+#include <AiwCommon.hrh>
+
+#include <aknnotewrappers.h>
+#include <DocumentHandler.h>
+#include <apmstd.h>
+#include <AknWaitDialog.h>
+#include <aknnotewrappers.h>
+#include <upnpdlnaprotocolinfo.h>
+#include <upnpcommonui.rsg>
+#include <utf.h>
+#include <bautils.h>
+
+// upnp specific MACRO definition
+#include "upnpconstantdefs.h"
+// upnp stack api
+#include <upnpitem.h>
+#include <upnpobject.h>
+#include <upnpstring.h>
+
+// upnp framework / avcontroller api
+#include "upnpavcontroller.h"                   // MUPnPAVController
+#include "upnpfiledownloadsession.h"            // MUPnPFileDownloadSession
+#include "upnpavbrowsingsession.h"
+
+// upnp framework / avcontroller helper api
+#include "upnpdlnautility.h"
+#include "upnpitemutility.h"
+#include "upnpfileutility.h"
+
+// upnp framework / internal api's
+#include "upnpcommonutils.h"
+#include "upnpsettingsengine.h" // get selected download location
+
+// USER INCLUDE FILES
+#include "upnpcommonui.h"
+#include "upnplocalplayer.h"
+#include "upnpdeviceobserver.h"
+
+// DEBUG
+_LIT( KComponentLogfile, "commonui.txt");
+#include "upnplog.h"
+
+// CONSTANT DEFINITIONS
+_LIT8( KProtocolInfo,   "protocolInfo" );
+_LIT8( KHttpDes,        "http://" );
+_LIT8( KHttpGetDes, "http-get" );
+_LIT8( KHttpEqual, "=" );
+
+const TInt KDownloadPosition = 0;
+
+// Video mimetypes that can be played on device
+
+
+// ============================ MEMBER FUNCTIONS ============================
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::CUPnPLocalPlayer
+// C++ default constructor can NOT contain any code, that
+// might leave.
+// --------------------------------------------------------------------------
+//
+CUPnPLocalPlayer::CUPnPLocalPlayer( MUPnPAVController& aAVController,
+                                    MUPnPAVBrowsingSession& aBrowseSession,
+                                    CUPnPCommonUI& aCommonUI  ):
+                                    iCommonUI(aCommonUI)
+    {
+    __LOG( "CUPnPLocalPlayer::CUPnPLocalPlayer" );
+    iAVController = &aAVController;
+    iExitReason = KErrNone;
+    iBrowseSession = &aBrowseSession;
+    __LOG( "CUPnPLocalPlayer::CUPnPLocalPlayer-END" );
+    }
+
+    
+    
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::ConstructL
+// Symbian 2nd phase constructor can leave.
+// --------------------------------------------------------------------------
+//
+void CUPnPLocalPlayer::ConstructL()
+    {
+    __LOG( "CUPnPLocalPlayer::ConstructL" ); 
+    iDocumentHandler = CDocumentHandler::NewL();
+    iDocumentHandler->SetExitObserver( this );
+    User::LeaveIfError( iFs.Connect() );
+    iDownloadSession = &iAVController->StartDownloadSessionL(
+                                            iBrowseSession->Device() );
+    __LOG( "CUPnPLocalPlayer::ConstructL-END" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::NewL
+// Two-phased constructor.
+// --------------------------------------------------------------------------
+//
+EXPORT_C CUPnPLocalPlayer* CUPnPLocalPlayer::NewL(
+                                    MUPnPAVController& aAVController,
+                                    MUPnPAVBrowsingSession& aBrowseSession,
+                                    CUPnPCommonUI& aCommonUI  )
+    {
+    CUPnPLocalPlayer* self = new( ELeave )CUPnPLocalPlayer( aAVController,
+                                                            aBrowseSession,
+                                                            aCommonUI );
+    CleanupStack::PushL(self);
+    self->ConstructL();
+    CleanupStack::Pop( self );
+    __LOG( "CUPnPLocalPlayer::NewL-END" );
+    return self;
+    }    
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::~CUPnPLocalPlayer
+// Destructor.
+// --------------------------------------------------------------------------
+//
+CUPnPLocalPlayer::~CUPnPLocalPlayer()
+    {   
+    __LOG( "CUPnPLocalPlayer::~CUPnPLocalPlayer" );
+    
+    // If download session is running, stop it
+    if( iAVController &&
+        iDownloadSession )
+        {
+        iAVController->StopDownloadSession( *iDownloadSession );
+        }
+
+    delete iDocumentHandler;
+    
+    delete iWaitNoteDialog;
+    
+    delete iItem;
+    
+    if( iFilePath )
+        {
+        iFs.Delete( *iFilePath );
+        delete iFilePath;
+        }
+    iFs.Close();
+
+    __LOG( "CUPnPLocalPlayer::~CUPnPLocalPlayer-END" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::PlayL
+// Play a selected item.
+// --------------------------------------------------------------------------
+//
+EXPORT_C void CUPnPLocalPlayer::PlayL( const CUpnpObject& aItem )
+    {
+    __LOG( "CUPnPLocalPlayer::PlayL" );
+    
+    // recreate iItem
+    delete iItem; iItem = NULL;
+    iItem = CUpnpItem::NewL();
+    iItem->CopyL( aItem );
+
+    if ( !IsLocallySupportedL( *iItem ) ) 
+        {
+        User::Leave( KErrNotSupported );
+        }
+    
+    //in order not to get two callbacks when MS is lost, 
+    // in CUPnPLocalPlayer and CUPnPBrowseDialog
+    iBrowseSessionObserver = iBrowseSession->Observer();
+    iBrowseSession->RemoveObserver(); 
+    iFs.Close();
+    
+    User::LeaveIfError( iFs.Connect() );
+    User::LeaveIfError( iFs.ShareProtected() );
+    delete iFilePath; iFilePath = NULL;
+    iExitReason = KErrNone;
+    iDownloadSession->SetObserver( *this );
+    
+    /**
+     * All the temporiarily downloaded files should go to the
+     * hidden folder \data\download\media\temp
+     * fix for ETLU-7LKCJB
+     */
+    //create a file path which should contain the absolute file path
+    //e.g c:\data\download\media\temp\image.jpg
+    
+    iFilePath = HBufC::NewL( KMaxFileName );
+    HBufC* copyLocation = HBufC::NewLC( KMaxFileName );
+    CUPnPSettingsEngine* settingsEngine = CUPnPSettingsEngine::NewL();
+    CleanupStack::PushL( settingsEngine );
+    TBool copyLocationIsPhoneMemory = 0; // not used in this case
+    TPtr copyLocationPtr( copyLocation->Des() );
+    settingsEngine->GetCopyLocationL( copyLocationPtr,
+        copyLocationIsPhoneMemory );
+        
+    CleanupStack::PopAndDestroy( settingsEngine );    
+        
+    iFilePath->Des().Append( *copyLocation );
+    CleanupStack::PopAndDestroy( copyLocation );
+    
+    _LIT( KTempFolder, "temp\\");
+    iFilePath->Des().Append( KTempFolder() );
+    
+    //check the existence of the target folder
+    if( !BaflUtils::FolderExists( iFs, *iFilePath ) )
+        {
+        User::LeaveIfError( iFs.MkDirAll( *iFilePath ) );
+        }
+        
+    User::LeaveIfError( iFs.SetAtt( *iFilePath, 
+                                    KEntryAttHidden, 
+                                    KEntryAttNormal ) );     
+    
+    //Get the title of the given item
+    HBufC* title16 = UpnpString::ToUnicodeL( aItem.Title() );
+    CleanupStack::PushL( title16 );
+    HBufC* title16checked =
+        UPnPCommonUtils::ReplaceIllegalFilenameCharactersL( *title16 );
+    CleanupStack::PopAndDestroy( title16 );
+    
+    iFilePath->Des().Append( *title16checked );
+    delete title16checked; title16checked = NULL;
+    //Get the extension of the given item
+    
+    const CUpnpItem* item = (CUpnpItem*)(&aItem);
+    const CUpnpElement* tmpEl = &( UPnPItemUtility::ResourceFromItemL( 
+        *item ) );
+    
+    if( !UPnPFileUtility::FitsInMemory( *tmpEl) )
+        {
+        User::Leave( KErrDiskFull );
+        }  
+            
+    const CUpnpAttribute* tmpAttInfo = UPnPItemUtility::FindAttributeByName(
+        *tmpEl, KAttributeProtocolInfo );
+    
+    User::LeaveIfNull( const_cast<CUpnpAttribute*>(tmpAttInfo ) );
+    
+    CUpnpDlnaProtocolInfo* tmpProtocolInfo = CUpnpDlnaProtocolInfo::NewL(
+        tmpAttInfo->Value() );
+   
+    CleanupStack::PushL( tmpProtocolInfo );
+        
+    HBufC* fileExt = NULL;
+    
+    fileExt = UPnPCommonUtils::FileExtensionByMimeTypeL(
+        tmpProtocolInfo->ThirdField() );
+    
+    User::LeaveIfNull( fileExt );
+    
+    iFilePath->Des().Append( *fileExt );
+    
+    delete fileExt; fileExt = NULL;
+    
+    CleanupStack::PopAndDestroy( tmpProtocolInfo );
+        
+    TInt err = KErrNone;
+    
+    RFile rfile;
+    err = rfile.Create(iFs, *iFilePath, EFileWrite );
+    
+    CleanupClosePushL( rfile );
+    if( KErrAlreadyExists == err )
+        {
+        __LOG( "Already exists -> Delete old and create new" );
+        User::LeaveIfError( iFs.Delete( *iFilePath ) );
+        User::LeaveIfError( rfile.Create(iFs, *iFilePath, EFileWrite ) );
+        }
+    
+    iDownloadSession->StartDownloadL( *tmpEl, 
+                                      ( CUpnpItem& )aItem, 
+                                      rfile,
+                                      KDownloadPosition );
+    CleanupStack::PopAndDestroy(&rfile);
+    
+    iWaitingNote = EFalse;    
+    if( !iWaitingNote ) //if ReserveLocalMSServicesCompleted is not called
+        {               //immediately
+        iWaitingNote = ETrue;
+        StartWaitingNoteL();
+        }
+    
+    __LOG1( "CUPnPLocalPlayer::PlayL-END %d", iExitReason );
+    if( iExitReason != KErrNone )
+        {    
+        User::Leave( iExitReason );
+        }
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::TransferStarted.
+// --------------------------------------------------------------------------
+//
+void CUPnPLocalPlayer::TransferStarted( TInt aKey, TInt aStatus )
+    {
+    __LOG( "CUPnPLocalPlayer::TransferStarted" );
+    if( aStatus != KErrNone)
+        {
+        iExitReason = aStatus;    
+        }
+    else if( aKey != KDownloadPosition )
+        {
+        iExitReason = KErrGeneral;
+        }
+    
+    if( iExitReason != KErrNone )
+        {
+        FinishNote();
+        }
+    __LOG( "CUPnPLocalPlayer::TransferStarted-END" );
+    }
+    
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::TransferCompleted.
+// --------------------------------------------------------------------------
+//        
+void CUPnPLocalPlayer::TransferCompleted( TInt aKey,
+                                          TInt aStatus,
+                                          const TDesC& aFilePath )
+    {
+    __LOG( "CUPnPLocalPlayer::TransferCompleted" );
+    
+    if( aKey != KDownloadPosition )
+        {
+        iExitReason = KErrGeneral;
+        FinishNote();
+        }
+    else
+        {
+        TRAP_IGNORE( CopyCompleteL( aStatus, aFilePath ) );    
+        }    
+    
+    __LOG( "CUPnPLocalPlayer::TransferCompleted-END" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::CopyCompleteL
+// Returns from UPnP AV control point when a copy operation has been finished
+// --------------------------------------------------------------------------
+void CUPnPLocalPlayer::CopyCompleteL( TInt aError,
+                                     const TDesC& /*aFilePath*/ )
+    {
+    __LOG1( "CopyCompleteL %d", aError );
+
+    FinishNote();
+    // If copying was successful, play the copied item
+    if( KErrNone == aError )
+        {
+        // iFilePath Contains UTF8 content,we need change
+        // to TDesC8 first ,Using Copy don't lost any data,because the 
+        //low byte is NULL       
+        TBuf8<KMaxFileName> filename;
+        filename.Copy( *iFilePath );
+        
+        HBufC* temp = iFilePath;
+        iFilePath = NULL;      
+        
+        // try transform UTF8 to UniCode
+        HBufC* unicodename = CnvUtfConverter::ConvertToUnicodeFromUtf8L(
+            filename );       
+        CleanupStack::PushL( unicodename );
+
+        // Rename the file
+        iFilePath = UPnPCommonUtils::RenameFileL( *unicodename ) ;
+        if( iFilePath )
+            {
+            delete temp; temp = NULL;
+            }
+        else
+            {
+            iFilePath = temp;
+            }    
+        CleanupStack::PopAndDestroy( unicodename );
+        unicodename = NULL;
+        
+        if( iFilePath )
+            {
+            
+            // fix for TSW: ESLX-7L3DMX
+            // OpenFileEmbeddedL( aSharableFile,aDataType, aParamList)
+            // leaves with KErrInUse which results into immediate close
+            // of image viewer.
+            
+            RFile sharableFile;
+            TRAPD( err, iDocumentHandler->OpenTempFileL( 
+                    *iFilePath, sharableFile ) );
+                    
+            if ( err == KErrNone )
+                {
+                CleanupClosePushL( sharableFile );
+
+                // Create a param list to remove 
+                // the "Use image as" sub menu item
+                CAiwGenericParamList* paramList = CAiwGenericParamList::NewLC();
+                paramList->Reset();
+                
+//       Append a param into the list to restrict the viewer application from
+//       showing the "use image as" sub menu item. Currently there is no
+//       AiwGenericParam for this. A CR has been created for AIW to add this
+//       constant to "AiwGenericParam.hrh".
+//          Example: This is how the "Save" menu item is allowed.
+//          paramList->AppendL( EGenericParamAllowSave );
+                
+                TDataType dataType = TDataType();
+                
+                __LOG( "Open document now... " );
+                            
+                TRAP( err, err = iDocumentHandler->OpenFileEmbeddedL(
+                            sharableFile, dataType, *paramList ) );
+
+                __LOG1( "err, err = iDocumentHandler->OpenFileEmbeddedL %d",
+                        err );
+                
+                // Cleanup
+                CleanupStack::PopAndDestroy( paramList );
+                CleanupStack::PopAndDestroy( &sharableFile );
+                }
+                                                    
+            if( KErrNone != err)
+                {
+                iFs.Delete( *iFilePath );
+                iExitReason = err;
+                }
+            }
+        else
+            {
+            iExitReason = KErrNoMemory;
+            }
+        }
+    else
+        {
+        iExitReason = aError;
+        }
+
+  
+    iCommonUI.HandleCommonErrorL( iExitReason, 0 );
+
+    __LOG( "CUPnPLocalPlayer::CopyCompleteL end" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::MediaServerDisappeared
+// Returns from UPnP AV control point when a media server disppears
+// --------------------------------------------------------------------------
+void CUPnPLocalPlayer::MediaServerDisappeared( 
+                                  TUPnPDeviceDisconnectedReason aReason )
+    {
+    __LOG( "CUPnPLocalPlayer::MediaServerDisappeared" );
+    TInt error = KErrNone;
+    if( aReason == EDisconnected )
+        {
+        error = KErrSessionClosed;
+        }
+    else if( aReason == EWLANLost)
+        {
+        error = KErrDisconnected;
+        }
+    else
+        {
+        __PANICD( __FILE__, __LINE__);
+        }
+    iExitReason = error;
+    FinishNote(); 
+    __LOG1("CUPnPLocalPlayer::MediaServerDisappeared %d END",error );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::HandleServerAppExit
+// Returns from application server after quitting a application
+// here is either image player or video player or music player
+// --------------------------------------------------------------------------
+void CUPnPLocalPlayer::HandleServerAppExit( TInt aReason )
+    {
+    __LOG1( "CUPnPLocalPlayer::HandleServerAppExit %d" , aReason );
+
+    if( iFilePath )
+        {
+        iFs.Delete( *iFilePath );
+        }
+
+    __LOG( "CUPnPLocalPlayer::HandleServerAppExit" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::DialogDismissedL
+// Returns from dialog server after cancelling a dialog
+// here is the wait note
+// --------------------------------------------------------------------------
+void CUPnPLocalPlayer::DialogDismissedL( TInt aButtonId )
+    {
+    __LOG1( "CUPnPLocalPlayer::DialogDismissedL %d", aButtonId );
+    if( aButtonId == EEikBidCancel )
+        {
+        iDownloadSession->CancelAllTransfers();
+        iDownloadSession->RemoveObserver();
+        if( iBrowseSessionObserver )
+            {
+            iBrowseSession->SetObserver( *iBrowseSessionObserver );
+            }
+        iBrowseSessionObserver = NULL;
+        iExitReason = KErrCancel;
+        __LOG( "CUPnPLocalPlayer::DialogDismissedL Cancel " );   
+
+        }
+   __LOG( "CUPnPLocalPlayer::DialogDismissedL" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::FinishNote
+// Finish the current waiting note and ready to quit the local playback
+// --------------------------------------------------------------------------
+void CUPnPLocalPlayer::FinishNote()
+    {
+    __LOG( "CUPnPLocalPlayer::FinishNote" );
+    iDownloadSession->CancelAllTransfers();
+    iDownloadSession->RemoveObserver();
+    if( iBrowseSessionObserver )
+        {
+        iBrowseSession->SetObserver( *iBrowseSessionObserver );
+        }
+    iBrowseSessionObserver = NULL;
+    if( iWaitNoteDialog )
+        {
+        TRAP_IGNORE( iWaitNoteDialog->ProcessFinishedL() );
+        delete iWaitNoteDialog;
+        iWaitNoteDialog = NULL;
+        }
+    __LOG( "CUPnPLocalPlayer::FinishNote-END" );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::StartWaitingNoteL
+// Start the current waiting note
+// -------------------------------------------------------------------------- 
+void CUPnPLocalPlayer::StartWaitingNoteL()
+    {
+    iWaitNoteDialog = new ( ELeave )CAknWaitDialog(
+                    ( REINTERPRET_CAST ( CEikDialog**, &iWaitNoteDialog ) ),
+                                                            ETrue);
+    iWaitNoteDialog->SetCallback( this );
+    iWaitNoteDialog->ExecuteLD( 
+                R_UPNPCOMMONUI_VIDEO_PLAYBACK_WAIT_NOTE_DIALOG );
+    }
+
+// --------------------------------------------------------------------------
+// CUPnPLocalPlayer::IsLocallySupportedL
+// Checks if the item can be played locally.
+// -------------------------------------------------------------------------- 
+TBool CUPnPLocalPlayer::IsLocallySupportedL( CUpnpObject& aItem )
+    {
+    TBool retval = EFalse; // return value
+
+    // Get all res elements into array
+    RUPnPElementsArray elms;
+    CleanupClosePushL( elms );
+    UPnPItemUtility::GetResElements( aItem, elms );
+    TInt count = elms.Count();
+    
+    CUpnpDlnaProtocolInfo* pInfo = NULL;
+
+    // Determine which resources are usable:
+    //  1. Filter out other than HTTP GET resources (internal uri's, RTP)
+    //  2. Filter out such resources for which DLNA profile is not supported.
+    //  3. Filter out such resources for which mime type is not supported.
+    for( TInt i = count-1 ; i >= 0; i-- )
+        {
+        // Make sure that it is a HTTP GET resource. Otherwise remove it and
+        // continue with the next one.
+        if( elms[ i ]->Value().Left( 
+                KHttpDes.iTypeLength ).Compare( KHttpDes() ) != 0 )
+            {
+            CUpnpElement* destroyable = elms[i];
+            aItem.RemoveElementL( destroyable ); 
+            delete destroyable;
+
+            continue;
+            }
+
+        // Obtain protocolInfo of the res element.
+        const CUpnpAttribute* attr = NULL;
+        TRAPD( nosuchattribute, 
+               attr = &UPnPItemUtility::FindAttributeByNameL(
+                                                        *elms[ i ],
+                                                        KProtocolInfo() ) );
+
+        if ( nosuchattribute ) 
+            {
+            // No mandatory protocolinfo attribute. Remove this and continue.
+            CUpnpElement* destroyable = elms[i];
+            aItem.RemoveElementL( destroyable ); 
+            delete destroyable;
+
+            continue;
+            }
+
+        // parse protocol info
+        pInfo = CUpnpDlnaProtocolInfo::NewL( attr->Value() );
+        CleanupStack::PushL( pInfo );
+
+        // Check that DLNA profile is among the supported ones.            
+        if ( pInfo->PnParameter() != KNullDesC8() 
+             && !UPnPDlnaUtility::IsSupportedDlnaProfile( 
+                                                pInfo->PnParameter() ) )
+            {
+            // DLNA profile not supported. Remove this and continue.
+            CUpnpElement* destroyable = elms[i];
+            aItem.RemoveElementL( destroyable ); 
+            delete destroyable;
+            CleanupStack::PopAndDestroy( pInfo );
+            pInfo = NULL;
+
+            continue;
+            }
+
+        // check that mime type is among the supported ones
+        TPtrC8 mime = pInfo->ThirdField();
+        if ( !UPnPDlnaUtility::IsSupportedMimeType( mime ) ) 
+            {
+            // mime type not supported.
+            TPtrC8 httpget = pInfo->FirstField();
+            TPtrC8 httpdlnatem = pInfo->FourthField();
+            HBufC8* tem = NULL;
+            tem = httpdlnatem.Right( httpdlnatem.Length() - httpdlnatem.Find( 
+                                            KHttpEqual ) - 1 ).AllocLC();
+            tem->Des().Trim();
+            TPtrC8 httpdlna = *tem;
+            CleanupStack::PopAndDestroy( tem );
+            if ( httpget.Compare( KHttpGetDes ) != 0 ||
+                    !UPnPDlnaUtility::IsSupportedDlnaProfile
+                    ( httpdlna ) )
+                {
+                // mime type not supported and DLNA profile not supported. 
+                // Remove this and continue
+                CUpnpElement* destroyable = elms[i];
+                aItem.RemoveElementL( destroyable ); 
+                delete destroyable;
+                CleanupStack::PopAndDestroy( pInfo );
+                pInfo = NULL;
+                
+                continue;
+                }
+            }
+
+        CleanupStack::PopAndDestroy( pInfo );
+        pInfo = NULL;
+        }
+
+    // All res elements have been processed and removed if they are not 
+    // supported. Clean up and return ETrue if there are res elements 
+    // left in the item and EFalse if there are no res elements left.        
+    CleanupStack::PopAndDestroy( &elms ); 
+
+    UPnPItemUtility::GetResElements( aItem, elms );
+    retval = elms.Count();
+    elms.Close();    
+
+    return retval;
+    }
+
+
+// End of File