Bug 803614 - [b2g-bluetooth] Save received file, r=qdot, r=dougt
authorEric Chou <echou@mozilla.com>
Wed, 31 Oct 2012 11:15:12 +0800
changeset 111990 76ee4691901159397416d48924c8bdee73e1d44f
parent 111989 e0271552b1b0ee69bb9a202380a897be29313a70
child 111991 543b9865a352f7cab09d463b949edf0eae6312d2
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersqdot, dougt
bugs803614
milestone19.0a1
Bug 803614 - [b2g-bluetooth] Save received file, r=qdot, r=dougt
dom/bluetooth/BluetoothOppManager.cpp
dom/bluetooth/BluetoothOppManager.h
dom/bluetooth/ObexBase.cpp
dom/bluetooth/ObexBase.h
--- a/dom/bluetooth/BluetoothOppManager.cpp
+++ b/dom/bluetooth/BluetoothOppManager.cpp
@@ -14,17 +14,23 @@
 #include "ObexBase.h"
 
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
+#include "nsIDOMFile.h"
+#include "nsIFile.h"
 #include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsNetUtil.h"
+
+#define TARGET_FOLDER "/sdcard/download/bluetooth/"
 
 USING_BLUETOOTH_NAMESPACE
 using namespace mozilla;
 using namespace mozilla::ipc;
 
 class BluetoothOppManagerObserver : public nsIObserver
 {
 public:
@@ -63,22 +69,30 @@ public:
   }
 };
 
 namespace {
 // Sending system message "bluetooth-opp-update-progress" every 50kb
 static const uint32_t kUpdateProgressBase = 50 * 1024;
 StaticRefPtr<BluetoothOppManager> sInstance;
 StaticRefPtr<BluetoothOppManagerObserver> sOppObserver;
-static nsCOMPtr<nsIInputStream> stream = nullptr;
+
+/*
+ * FIXME / Bug 806749
+ *
+ * Currently Bluetooth*Manager inherits mozilla::ipc::UnixSocketConsumer,
+ * which means that each Bluetooth*Manager can handle only one socket
+ * connection at a time. We need to support concurrent multiple socket
+ * connections, and then we will be able to have multiple file transferring
+ * sessions at a time.
+ */
 static uint32_t sSentFileLength = 0;
 static nsString sFileName;
 static uint32_t sFileLength = 0;
 static nsString sContentType;
-static int sUpdateProgressCounter = 0;
 static bool sInShutdown = false;
 }
 
 NS_IMETHODIMP
 BluetoothOppManagerObserver::Observe(nsISupports* aSubject,
                                      const char* aTopic,
                                      const PRUnichar* aData)
 {
@@ -90,48 +104,34 @@ BluetoothOppManagerObserver::Observe(nsI
 
   MOZ_ASSERT(false, "BluetoothOppManager got unexpected topic!");
   return NS_ERROR_UNEXPECTED;
 }
 
 class ReadFileTask : public nsRunnable
 {
 public:
-  ReadFileTask(nsIDOMBlob* aBlob) : mBlob(aBlob)
+  ReadFileTask(nsIInputStream* aInputStream) : mInputStream(aInputStream)
   {
     MOZ_ASSERT(NS_IsMainThread());
   }
 
   NS_IMETHOD Run()
   {
-    if (NS_IsMainThread()) {
-      NS_WARNING("Can't read file from main thread");
-      return NS_ERROR_FAILURE;
-    }
-
-    nsresult rv;
-
-    if (stream == nullptr) {
-      rv = mBlob->GetInternalStream(getter_AddRefs(stream));
-      if (NS_FAILED(rv)) {
-        NS_WARNING("Can't get internal stream of blob");
-        return NS_ERROR_FAILURE;
-      }
-    }
+    MOZ_ASSERT(!NS_IsMainThread());
 
     /*
      * 255 is the Minimum OBEX Packet Length (See section 3.3.1.4,
      * IrOBEX ver 1.2)
      */
     char buf[255];
     uint32_t numRead;
-    int offset = 0;
 
     // function inputstream->Read() only works on non-main thread
-    rv = stream->Read(buf, sizeof(buf), &numRead);
+    nsresult rv = mInputStream->Read(buf, sizeof(buf), &numRead);
     if (NS_FAILED(rv)) {
       // Needs error handling here
       return NS_ERROR_FAILURE;
     }
 
     if (numRead > 0) {
       if (sSentFileLength + numRead >= sFileLength) {
         sInstance->SendPutRequest((uint8_t*)buf, numRead, true);
@@ -141,28 +141,26 @@ public:
 
       sSentFileLength += numRead;
     }
 
     return NS_OK;
   };
 
 private:
-  nsCOMPtr<nsIDOMBlob> mBlob;
+  nsCOMPtr<nsIInputStream> mInputStream;
 };
 
 BluetoothOppManager::BluetoothOppManager() : mConnected(false)
                                            , mConnectionId(1)
                                            , mLastCommand(0)
-                                           , mBlob(nullptr)
                                            , mRemoteObexVersion(0)
                                            , mRemoteConnectionFlags(0)
                                            , mRemoteMaxPacketLength(0)
                                            , mAbortFlag(false)
-                                           , mReadFileThread(nullptr)
                                            , mPacketLeftLength(0)
                                            , mReceiving(false)
                                            , mPutFinal(false)
                                            , mWaitingForConfirmationFlag(false)
 {
   // FIXME / Bug 800249:
   //   mConnectedDeviceAddress is Bluetooth address of connected device,
   //   we will be able to get this value after bug 800249 lands. For now,
@@ -297,23 +295,65 @@ BluetoothOppManager::ConfirmReceivingFil
   }
 
   NS_ASSERTION(mPacketLeftLength == 0,
                "Should not be in the middle of receiving a PUT packet.");
 
   mWaitingForConfirmationFlag = false;
   ReplyToPut(mPutFinal, aConfirm);
 
+  if (aConfirm) {
+    StartFileTransfer(mConnectedDeviceAddress, true,
+                      sFileName, sFileLength, sContentType);
+  }
+
   if (mPutFinal || !aConfirm) {
     mReceiving = false;
     FileTransferComplete(mConnectedDeviceAddress, aConfirm, true, sFileName,
                          sSentFileLength, sContentType);
   }
 }
 
+void
+BluetoothOppManager::AfterOppConnected()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  mConnected = true;
+  mUpdateProgressCounter = 1;
+  sSentFileLength = 0;
+  mAbortFlag = false;
+}
+
+void
+BluetoothOppManager::AfterOppDisconnected()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  mConnected = false;
+  mReceiving = false;
+  mLastCommand = 0;
+  mBlob = nullptr;
+
+  if (mInputStream) {
+    mInputStream->Close();
+    mInputStream = nullptr;
+  }
+
+  if (mOutputStream) {
+    mOutputStream->Close();
+    mOutputStream = nullptr;
+  }
+
+  if (mReadFileThread) {
+    mReadFileThread->Shutdown();
+    mReadFileThread = nullptr;
+  }
+}
+
 // Virtual function of class SocketConsumer
 void
 BluetoothOppManager::ReceiveSocketData(UnixSocketRawData* aMessage)
 {
   uint8_t opCode;
   int packetLength;
   int receivedLength = aMessage->mSize;
 
@@ -322,17 +362,17 @@ BluetoothOppManager::ReceiveSocketData(U
     packetLength = mPacketLeftLength;
   } else {
     opCode = aMessage->mData[0];
     packetLength = (((int)aMessage->mData[1]) << 8) | aMessage->mData[2];
   }
 
   if (mLastCommand == ObexRequestCode::Connect) {
     if (opCode == ObexResponseCode::Success) {
-      mConnected = true;
+      AfterOppConnected();
 
       // Keep remote information
       mRemoteObexVersion = aMessage->mData[3];
       mRemoteConnectionFlags = aMessage->mData[4];
       mRemoteMaxPacketLength =
         (((int)(aMessage->mData[5]) << 8) | aMessage->mData[6]);
 
       if (mBlob) {
@@ -377,113 +417,187 @@ BluetoothOppManager::ReceiveSocketData(U
         sFileLength = fileLength;
 
         if (NS_FAILED(NS_NewThread(getter_AddRefs(mReadFileThread)))) {
           NS_WARNING("Can't create thread");
           SendDisconnectRequest();
           return;
         }
 
-        sUpdateProgressCounter = 1;
-        sSentFileLength = 0;
-        mAbortFlag = false;
         sInstance->SendPutHeaderRequest(sFileName, sFileLength);
         StartFileTransfer(mConnectedDeviceAddress, false,
                           sFileName, sFileLength, sContentType);
       }
     }
   } else if (mLastCommand == ObexRequestCode::Disconnect) {
     if (opCode != ObexResponseCode::Success) {
       // FIXME: Needs error handling here
       NS_WARNING("[OPP] Disconnect failed");
-    } else {
-      mConnected = false;
-      mReceiving = false;
-      mLastCommand = 0;
-      mBlob = nullptr;
-      mReadFileThread = nullptr;
     }
+
+    AfterOppDisconnected();
   } else if (mLastCommand == ObexRequestCode::Put) {
     if (opCode != ObexResponseCode::Continue) {
       // FIXME: Needs error handling here
       NS_WARNING("[OPP] Put failed");
-    } else {
-      if (mAbortFlag || mReadFileThread == nullptr) {
-        SendAbortRequest();
-      } else {
-        if (kUpdateProgressBase * sUpdateProgressCounter < sSentFileLength) {
-          UpdateProgress(mConnectedDeviceAddress, false,
-                         sSentFileLength, sFileLength);
-          ++sUpdateProgressCounter;
-        }
+      return;
+    }
+
+    if (mAbortFlag || mReadFileThread) {
+      SendAbortRequest();
+      return;
+    }
 
-        nsRefPtr<ReadFileTask> task = new ReadFileTask(mBlob);
+    if (kUpdateProgressBase * mUpdateProgressCounter < sSentFileLength) {
+      UpdateProgress(mConnectedDeviceAddress, false,
+                     sSentFileLength, sFileLength);
+      mUpdateProgressCounter = sSentFileLength / kUpdateProgressBase + 1;
+    }
 
-        if (NS_FAILED(mReadFileThread->Dispatch(task, NS_DISPATCH_NORMAL))) {
-          NS_WARNING("Cannot dispatch ring task!");
-        }
+    if (mInputStream) {
+      nsresult rv = mBlob->GetInternalStream(getter_AddRefs(mInputStream));
+      if (NS_FAILED(rv)) {
+        NS_WARNING("Can't get internal stream of blob");
+        return;
       }
     }
+
+    nsRefPtr<ReadFileTask> task = new ReadFileTask(mInputStream);
+    if (NS_FAILED(mReadFileThread->Dispatch(task, NS_DISPATCH_NORMAL))) {
+      NS_WARNING("Cannot dispatch ring task!");
+    }
   } else if (mLastCommand == ObexRequestCode::PutFinal) {
     if (opCode != ObexResponseCode::Success) {
       // FIXME: Needs error handling here
       NS_WARNING("[OPP] PutFinal failed");
-    } else {
-      FileTransferComplete(mConnectedDeviceAddress, true, false, sFileName,
-                           sSentFileLength, sContentType);
-      SendDisconnectRequest();
+      return;
     }
+
+    FileTransferComplete(mConnectedDeviceAddress, true, false, sFileName,
+                         sSentFileLength, sContentType);
+    SendDisconnectRequest();
   } else if (mLastCommand == ObexRequestCode::Abort) {
     if (opCode != ObexResponseCode::Success) {
       NS_WARNING("[OPP] Abort failed");
     }
 
     FileTransferComplete(mConnectedDeviceAddress, false, false, sFileName,
                          sSentFileLength, sContentType);
     SendDisconnectRequest();
   } else {
     // Remote request or unknown mLastCommand
     ObexHeaderSet pktHeaders(opCode);
 
     if (opCode == ObexRequestCode::Connect) {
-      ParseHeaders(&aMessage->mData[7], receivedLength - 7, &pktHeaders);
+      // Section 3.3.1 "Connect", IrOBEX 1.2
+      // [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
+      // [Headers:var]
+      ParseHeadersAndFindBody(&aMessage->mData[7],
+                              receivedLength - 7,
+                              &pktHeaders);
       ReplyToConnect();
+      AfterOppConnected();
     } else if (opCode == ObexRequestCode::Disconnect) {
-      ParseHeaders(&aMessage->mData[3], receivedLength - 3, &pktHeaders);
+      // Section 3.3.2 "Disconnect", IrOBEX 1.2
+      // [opcode:1][length:2][Headers:var]
+      ParseHeadersAndFindBody(&aMessage->mData[3],
+                              receivedLength - 3,
+                              &pktHeaders);
       ReplyToDisconnect();
+      AfterOppDisconnected();
     } else if (opCode == ObexRequestCode::Put ||
                opCode == ObexRequestCode::PutFinal) {
+      // Section 3.3.3 "Put", IrOBEX 1.2
+      // [opcode:1][length:2][Headers:var]
+      int headerStartIndex = 3;
+
       if (!mReceiving) {
+        nsString path;
+        path.AssignLiteral(TARGET_FOLDER);
+
         MOZ_ASSERT(mPacketLeftLength == 0);
-        ParseHeaders(&aMessage->mData[3], receivedLength - 3, &pktHeaders);
+        ParseHeadersAndFindBody(&aMessage->mData[headerStartIndex],
+                                receivedLength - headerStartIndex,
+                                &pktHeaders);
 
         pktHeaders.GetName(sFileName);
         pktHeaders.GetContentType(sContentType);
         pktHeaders.GetLength(&sFileLength);
 
+        path += sFileName;
+
+        nsCOMPtr<nsIFile> f;
+        nsresult rv = NS_NewLocalFile(path, false, getter_AddRefs(f));
+        if (NS_FAILED(rv)) {
+          NS_WARNING("Couldn't new a local file");
+        }
+
+        rv = f->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00644);
+        if (NS_FAILED(rv)) {
+          NS_WARNING("Couldn't create the file");
+        }
+
+        NS_NewLocalFileOutputStream(getter_AddRefs(mOutputStream), f);
+        if (!mOutputStream) {
+          NS_WARNING("Couldn't new an output stream");
+        }
+
         mReceiving = true;
         mWaitingForConfirmationFlag = true;
       }
 
       /*
        * A PUT request from remote devices may be divided into multiple parts.
        * In other words, one request may need to be received multiple times,
        * so here we keep a variable mPacketLeftLength to indicate if current
        * PUT request is done.
        */
       mPutFinal = (opCode == ObexRequestCode::PutFinal);
 
+      uint32_t wrote = 0;
       if (mPacketLeftLength == 0) {
-        NS_ASSERTION(mPacketLeftLength >= receivedLength,
+        NS_ASSERTION(packetLength >= receivedLength,
                      "Invalid packet length");
         mPacketLeftLength = packetLength - receivedLength;
+
+        int headerBodyOffset =
+          ParseHeadersAndFindBody(&aMessage->mData[headerStartIndex],
+                                  receivedLength - headerStartIndex,
+                                  &pktHeaders);
+
+        if (headerBodyOffset != -1) {
+          /*
+           * Adding by 3 is because the format of a header is like:
+           *     [HeaderId:1 (BODY)][HeaderLength:2][Data:n]
+           * and headerStartIndex + headerBodyOffset points to HeaderId,
+           * so adding 3 is to point to the beginning of data.
+           *
+           */
+          int fileBodyIndex = headerStartIndex + headerBodyOffset + 3;
+
+          mOutputStream->Write((char*)&aMessage->mData[fileBodyIndex],
+                               receivedLength - fileBodyIndex, &wrote);
+          NS_ASSERTION(receivedLength - fileBodyIndex == wrote,
+                       "Writing to the file failed");
+        }
       } else {
         NS_ASSERTION(mPacketLeftLength >= receivedLength,
                      "Invalid packet length");
         mPacketLeftLength -= receivedLength;
+
+        mOutputStream->Write((char*)&aMessage->mData[0], receivedLength, &wrote);
+        NS_ASSERTION(receivedLength == wrote, "Writing to the file failed");
+      }
+
+      sSentFileLength += wrote;
+      if (sSentFileLength > kUpdateProgressBase * mUpdateProgressCounter &&
+          !mWaitingForConfirmationFlag) {
+        UpdateProgress(mConnectedDeviceAddress, true,
+                       sSentFileLength, sFileLength);
+        mUpdateProgressCounter = sSentFileLength / kUpdateProgressBase + 1;
       }
 
       if (mPacketLeftLength == 0) {
         if (mWaitingForConfirmationFlag) {
           ReceivingFileConfirmation(mConnectedDeviceAddress, sFileName,
                                     sFileLength, sContentType);
         } else {
           ReplyToPut(mPutFinal, true);
@@ -557,17 +671,16 @@ BluetoothOppManager::SendPutHeaderReques
   delete [] req;
 }
 
 void
 BluetoothOppManager::SendPutRequest(uint8_t* aFileBody,
                                     int aFileBodyLength,
                                     bool aFinal)
 {
-  int sentFileBodyLength = 0;
   int index = 3;
   int packetLeftSpace = mRemoteMaxPacketLength - index - 3;
 
   if (!mConnected) return;
   if (aFileBodyLength > packetLeftSpace) {
     NS_WARNING("Not allowed such a small MaxPacketLength value");
     return;
   }
--- a/dom/bluetooth/BluetoothOppManager.h
+++ b/dom/bluetooth/BluetoothOppManager.h
@@ -7,16 +7,19 @@
 #ifndef mozilla_dom_bluetooth_bluetoothoppmanager_h__
 #define mozilla_dom_bluetooth_bluetoothoppmanager_h__
 
 #include "BluetoothCommon.h"
 #include "mozilla/dom/ipc/Blob.h"
 #include "mozilla/ipc/UnixSocket.h"
 #include "nsIDOMFile.h"
 
+class nsIOutputStream;
+class nsIInputStream;
+
 BEGIN_BLUETOOTH_NAMESPACE
 
 class BluetoothReplyRunnable;
 
 class BluetoothOppManager : public mozilla::ipc::UnixSocketConsumer
 {
 public:
   /*
@@ -80,32 +83,37 @@ private:
                       uint32_t aFileLength);
   void ReceivingFileConfirmation(const nsString& aAddress,
                                  const nsString& aFileName,
                                  uint32_t aFileLength,
                                  const nsString& aContentType);
   void ReplyToConnect();
   void ReplyToDisconnect();
   void ReplyToPut(bool aFinal, bool aContinue);
+  void AfterOppConnected();
+  void AfterOppDisconnected();
   virtual void OnConnectSuccess() MOZ_OVERRIDE;
   virtual void OnConnectError() MOZ_OVERRIDE;
   virtual void OnDisconnect() MOZ_OVERRIDE;
 
   bool mConnected;
   int mConnectionId;
   int mLastCommand;
   uint8_t mRemoteObexVersion;
   uint8_t mRemoteConnectionFlags;
   int mRemoteMaxPacketLength;
   bool mAbortFlag;
   int mPacketLeftLength;
   nsString mConnectedDeviceAddress;
   bool mReceiving;
   bool mPutFinal;
   bool mWaitingForConfirmationFlag;
+  int mUpdateProgressCounter;
 
   nsCOMPtr<nsIDOMBlob> mBlob;
   nsCOMPtr<nsIThread> mReadFileThread;
+  nsCOMPtr<nsIOutputStream> mOutputStream;
+  nsCOMPtr<nsIInputStream> mInputStream;
 };
 
 END_BLUETOOTH_NAMESPACE
 
 #endif
--- a/dom/bluetooth/ObexBase.cpp
+++ b/dom/bluetooth/ObexBase.cpp
@@ -63,31 +63,42 @@ AppendHeaderConnectionId(uint8_t* retBuf
 void
 SetObexPacketInfo(uint8_t* retBuf, uint8_t opcode, int packetLength)
 {
   retBuf[0] = opcode;
   retBuf[1] = (packetLength & 0xFF00) >> 8;
   retBuf[2] = packetLength & 0x00FF;
 }
 
-void
-ParseHeaders(uint8_t* buf, int totalLength, ObexHeaderSet* retHandlerSet)
+int
+ParseHeadersAndFindBody(uint8_t* aHeaderStart,
+                        int aTotalLength,
+                        ObexHeaderSet* aRetHandlerSet)
 {
-  uint8_t* ptr = buf;
+  uint8_t* ptr = aHeaderStart;
+
+  while (ptr - aHeaderStart < aTotalLength) {
+    ObexHeaderId headerId = (ObexHeaderId)*ptr;
 
-  while (ptr - buf < totalLength) {
-    ObexHeaderId headerId = (ObexHeaderId)*ptr++;
+    if (headerId == ObexHeaderId::Body ||
+        headerId == ObexHeaderId::EndOfBody) {
+      return ptr - aHeaderStart;
+    }
+
+    ++ptr;
+
     int contentLength = 0;
     uint8_t highByte, lowByte;
 
     // Defined in 2.1 OBEX Headers, IrOBEX 1.2
     switch (headerId >> 6)
     {
       case 0x00:
-        // NULL terminated Unicode text, length prefixed with 2 byte unsigned integer.
+        // NULL terminated Unicode text, length prefixed with 2-byte
+        // unsigned integer.
       case 0x01:
         // byte sequence, length prefixed with 2 byte unsigned integer.
         highByte = *ptr++;
         lowByte = *ptr++;
         contentLength = (((int)highByte << 8) | lowByte) - 3;
         break;
 
       case 0x02:
@@ -96,23 +107,19 @@ ParseHeaders(uint8_t* buf, int totalLeng
         break;
 
       case 0x03:
         // 4 byte quantity
         contentLength = 4;
         break;
     }
 
-    // FIXME: This case should be happened when we are receiving header 'Body'
-    // (file body). I will handle this in another bug.
-    if (contentLength + (ptr - buf) > totalLength) {
-      break;
-    }
-
     uint8_t* content = new uint8_t[contentLength];
     memcpy(content, ptr, contentLength);
-    retHandlerSet->AddHeader(new ObexHeader(headerId, contentLength, content));
+    aRetHandlerSet->AddHeader(new ObexHeader(headerId, contentLength, content));
 
     ptr += contentLength;
   }
+
+  return -1;
 }
 
 END_BLUETOOTH_NAMESPACE
--- a/dom/bluetooth/ObexBase.h
+++ b/dom/bluetooth/ObexBase.h
@@ -137,16 +137,18 @@ public:
 
   void AddHeader(ObexHeader* aHeader)
   {
     mHeaders.AppendElement(aHeader);
   }
 
   void GetName(nsString& aRetName)
   {
+    aRetName.Truncate();
+
     int length = mHeaders.Length();
 
     for (int i = 0; i < length; ++i) {
       if (mHeaders[i]->mId == ObexHeaderId::Name) {
         uint8_t* ptr = mHeaders[i]->mData.get();
         int nameLength = mHeaders[i]->mDataLength / 2;
 
         for (int j = 0; j < nameLength; ++j) {
@@ -156,16 +158,18 @@ public:
 
         break;
       }
     }
   }
 
   void GetContentType(nsString& aRetContentType)
   {
+    aRetContentType.Truncate();
+
     int length = mHeaders.Length();
 
     for (int i = 0; i < length; ++i) {
       if (mHeaders[i]->mId == ObexHeaderId::Type) {
         uint8_t* ptr = mHeaders[i]->mData.get();
         aRetContentType.AssignASCII((const char*)ptr);
         break;
       }
@@ -191,13 +195,15 @@ public:
   }
 };
 
 int AppendHeaderName(uint8_t* retBuf, const char* name, int length);
 int AppendHeaderBody(uint8_t* retBuf, uint8_t* data, int length);
 int AppendHeaderLength(uint8_t* retBuf, int objectLength);
 int AppendHeaderConnectionId(uint8_t* retBuf, int connectionId);
 void SetObexPacketInfo(uint8_t* retBuf, uint8_t opcode, int packetLength);
-void ParseHeaders(uint8_t* buf, int totalLength, ObexHeaderSet* retHanderSet);
+int ParseHeadersAndFindBody(uint8_t* aHeaderStart,
+                            int aTotalLength,
+                            ObexHeaderSet* aRetHanderSet);
 
 END_BLUETOOTH_NAMESPACE
 
 #endif