Creating a TAPI Connection Using CTapiConnection

Nancy Winnick Cluts
Microsoft Developer Network Technology Group

November 1995
Revised: August 1996 (Changed function CheckAndReallocBuffer so that it zeroes out the memory allocated for TAPI structures.)

Click to open or copy the files in the DIALIT sample application.

Abstract

This article demonstrates how the developer can add some simple telephony capabilities to an application by using the Microsoft® Foundation Class Library (MFC). I created a class, CTapiConnection, that supports the basic functionality needed to use the Microsoft Windows® Telephony Application Programming Interface (TAPI) to automatically dial the telephone for a voice connection. It's a really good idea to read "TAP into the Future" (also available in the MSDN Library) before reading this article to familiarize yourself with the overall design of TAPI; otherwise, you may not understand some of the steps. This article takes the reader through the steps necessary to do the following:

The DIALIT Sample

When I decided to write a sample that uses the Telephony Application Programming Interface (TAPI), the first thing I did was look through the Microsoft® Win32® Software Development Kit (SDK) for any and all information about it. Happily, I found the Microsoft Telephony Programmer's Reference and the Microsoft Telephony Service Provider Interface (TSPI) for Telephony. The programmer's reference is intended to document the functionality that an application using TAPI will need. The service provider documentation is for developers who are going to write their own TAPI services (that is, vendors of telephony equipment).

With the documentation in hand, I also wanted to find some sample source code to look at. Tucked (or should I say buried?) in the Product Sample tree of the MSDN Library, I found the TAPICOMM (MSDN Library, Sample Code, Product Samples) sample, which demonstrates how to use TAPI for data transmission. It also demonstrates how to use the Communications (COMM) API and how to use multiple threads in your application. In short, it's terrific. Unfortunately, it's also a really big sample. To get to the generic TAPI code is a navigational challenge. As a result, I knew that it would be helpful to give you a simple sample and a Microsoft Foundation Class Library (MFC) class to help you learn about TAPI.

Thus, I decided to create a simple telephone dialer, and I came up with the DIALIT sample. It was written using Visual C++® 2.2 and MFC version 3.1. It is a Win32-based application that has been built and tested on Windows® 95. As you can see from Figure 1, the user interface is minimal. (Yes, you are reading this correctly. I really did write something non-GUI. Amazing!)

Figure 1. The DIALIT sample

The CTapiConnection Class

The CTapiConnection class was designed for the DIALIT sample to allow the application developer a simple method of establishing a TAPI connection. The class, as defined in the TAPIUTILS.H file, is shown below. The member functions will be described in more detail later in this article.

class CTapiConnection
{

 protected:
    // This area contains the protected members of the CTapiConnection class.
    DWORD m_dwNumDevs;      // the number of line devices available
    DWORD m_dwDeviceID;     // the line device ID
    DWORD m_dwRequestedID;
    LONG m_lAsyncReply;

    // BOOLEANS to handle reentrancy.
    BOOL m_bShuttingDown;
    BOOL m_bStoppingCall;
    BOOL m_bInitializing;
    BOOL m_bReplyReceived;
    BOOL m_bTapiInUse;     // whether TAPI is in use or not
    BOOL m_bInitialized;   // whether TAPI has been initialized

 public:
    // This area contains the public members of the CTapiConnection class.
    HLINEAPP m_hLineApp;       // the usage handle of this application for TAPI
    HCALL m_hCall;             // handle to the call
    HLINE m_hLine;             // handle to the open line
    DWORD m_dwAPIVersion;      // the API version
    char m_szPhoneNumber[64];  // the phone number to call

 protected:
    // Here is where I put the protected (internal) functions.
BOOL ShutdownTAPI();
    BOOL HandleLineErr(long lLineErr);
    LPLINEDEVCAPS GetDeviceLine(DWORD *dwAPIVersion, 
        LPLINEDEVCAPS lpLineDevCaps);
    LPLINEDEVCAPS MylineGetDevCaps(LPLINEDEVCAPS lpLineDevCaps,
        DWORD dwDeviceID, DWORD dwAPIVersion);
    LPVOID CheckAndReAllocBuffer(LPVOID lpBuffer, size_t sizeBufferMinimum);
    LPLINEADDRESSCAPS MylineGetAddressCaps (LPLINEADDRESSCAPS lpLineAddressCaps,
        DWORD dwDeviceID, DWORD dwAddressID, DWORD dwAPIVersion, 
        DWORD dwExtVersion);
    BOOL MakeTheCall(LPLINEDEVCAPS lpLineDevCaps,LPCSTR lpszAddress);
    LPLINECALLPARAMS CreateCallParams (LPLINECALLPARAMS lpCallParams, 
        LPCSTR lpszDisplayableAddress);
    long WaitForReply (long lRequestID);
    void HandleLineCallState(DWORD dwDevice, DWORD dwMessage, 
        DWORD dwCallbackInstance,
        DWORD dwParam1, DWORD dwParam2, DWORD dwParam3);
 private:
   // This section is for private functions.

 public:
    // Public functions.
    CTapiConnection();
    ~CTapiConnection();
    BOOL Create(char *szPhoneNumber = NULL);
    BOOL DialCall(char *szPhoneNumber = NULL);
    BOOL HangupCall();
    static void CALLBACK lineCallbackFunc(
        DWORD dwDevice, DWORD dwMsg, DWORD dwCallbackInstance, 
        DWORD dwParam1, DWORD dwParam2, DWORD dwParam3);

};

As you can glean from the class definition, implementing basic telephony requires a lot of work. But you will notice that the member functions that I have defined are simple:

Note   The line callback function is not called directly by the application using the class; rather, it is called by the system for line notifications. I'll talk more about the line callback function later in this article.

Step One: Initializing TAPI

The first thing an application must do before it uses any telephony services is to initialize TAPI. This means that the application must establish some way to communicate between itself and TAPI. TAPI uses a callback function to facilitate this communication. The application tells TAPI the address of this callback function when the application makes a call to lineInitialize.

The lineInitialize function fills in two values passed to it: a usage handle (shown in the following example as m_hLineApp) and the number of line devices available to the application (shown in the following example as m_dwNumDevs). If the call to lineInitialize is successful, a value of zero is returned. If an error occurs, a negative value is returned. All TAPI functions return a value of zero to signal success. To make it easier for me, I defined a constant called SUCCESS to be zero and used that in my code.

The following example is the code I wrote to initialize TAPI. It takes a pointer to a string containing the phone number to be dialed. By default, this pointer is NULL. The member function that actually dials the phone number, DialCall, also takes as a parameter the telephone number to dial. The user of the class can specify the number in either place.

BOOL CTapiConnection::Create(char *szPhoneNumber)
{
    long lReturn;

    // If we're already initialized, then initialization succeeds.
    if (m_hLineApp)
        return TRUE;

    // If we're in the middle of initializing, then fail, we're not done.
    if (m_bInitializing)
        return FALSE;

    m_bInitializing = TRUE;

    // Initialize TAPI.
    do
    {
        lReturn = ::lineInitialize(&m_hLineApp, 
            AfxGetInstanceHandle(), 
            lineCallbackFunc, 
            "DialIt", 
            &m_dwNumDevs);

        if (m_dwNumDevs == 0)
        {
            AfxMessageBox("There are no telephony devices installed.");
            m_bInitializing = FALSE;
            return FALSE;
        }

        if (HandleLineErr(lReturn))
            continue;
        else
        {
            OutputDebugString("lineInitialize unhandled error\n");
            m_bInitializing = FALSE;
            return FALSE;
        }
    }
    while(lReturn != SUCCESS);

    // If the user furnished a phone number, copy it over.
    if (szPhoneNumber)
        strcpy(m_szPhoneNumber, szPhoneNumber);

    m_bInitializing = FALSE;
    return TRUE;
}

Step Two: Obtaining a Line

Now that TAPI has been initialized, the application needs to interact with the user to know what type of call to make. The application does this by building a dialog box or property sheet using the standard Win32 functions. There isn't a handy common dialog for this. (Drats!) In the DIALIT sample, a simple dialog box lets the user enter the desired phone number to dial in the edit box and then click the Dial button to dial.

When we know the number to dial and that we should dial it, the application needs to obtain a line handle. This is done by a call to the lineOpen function. Before the application can make a call to lineOpen, though, it has to make sure that the line can support the type of call that the application is trying to make. In the case of the DIALIT sample, this is a line that is capable of voice transmission.

Getting the Line Capabilities

The application calls the lineGetDevCaps function to determine the capabilities of a given phone line. The function fills in a structure, LINEDEVCAPS, containing this information. Sounds straightforward, doesn't it. Well, there's a bit of a catch: the LINEDEVCAPS structure is defined by the telephony service provider and, as such, is variable length. After the application makes the call to lineGetDevCaps, it must check to see if the amount of space supplied for the structure was sufficient for the size of the provider's structure. This is done by comparing the dwNeededSize and dwTotalSize fields. If the needed size is larger than the total size, the application needs to pass a larger buffer to the function and try again. I created a function that calls lineGetDevCaps until a sufficient sized buffer is returned for this purpose. This function calls another function, CheckAndReAllocBuffer, that checks to see if the buffer exists and is large enough; it also fills in the dwTotalSize field of the LINEDEVCAPS structure. Filling in this field is imperative. If you don't set this correctly, the call to lineGetDevCaps will most likely fail. This function is called from the DialCall member function of the CTapiConnection class.

LPLINEDEVCAPS CTapiConnection::MylineGetDevCaps(
    LPLINEDEVCAPS lpLineDevCaps,
    DWORD dwDeviceID, DWORD dwAPIVersion)
{
    // Allocate enough space for the structure plus 1024.
    size_t sizeofLineDevCaps = sizeof(LINEDEVCAPS) + 1024;
    long lReturn;
    
    // Continue this loop until the structure is big enough.
    while(TRUE)
    {
        // Make sure the buffer exists, is valid, and is big enough.
        lpLineDevCaps = 
            (LPLINEDEVCAPS) CheckAndReAllocBuffer(
                (LPVOID) lpLineDevCaps,   // pointer to existing buffer, if any
                sizeofLineDevCaps);       // minimum size the buffer should be

        if (lpLineDevCaps == NULL)
            return NULL;

        // Make the call to fill the structure.
        do
        {
            lReturn = 
                ::lineGetDevCaps(m_hLineApp, dwDeviceID, 
                    dwAPIVersion, 0, lpLineDevCaps);

            if (HandleLineErr(lReturn))
                continue;
            else
            {
                LocalFree(lpLineDevCaps);
                return NULL;
            }
        }
        while (lReturn != SUCCESS);

        // If the buffer was big enough, then succeed.
        if ((lpLineDevCaps -> dwNeededSize) <= (lpLineDevCaps -> dwTotalSize))
            return lpLineDevCaps;

        // Buffer wasn't big enough. Make it bigger and try again.
        sizeofLineDevCaps = lpLineDevCaps -> dwNeededSize;
    }
}

LPVOID CTapiConnection::CheckAndReAllocBuffer(LPVOID lpBuffer, 
   size_t sizeBufferMinimum)
{
    size_t sizeBuffer;

    if (lpBuffer == NULL)   // allocate the buffer if necessary
    {
        sizeBuffer = sizeBufferMinimum;
        lpBuffer = (LPVOID) LocalAlloc (LPTR, sizeBuffer);

        if (lpBuffer == NULL)
        {
            OutputDebugString("LocalAlloc failed in CheckAndReAllocBuffer./n");
            return NULL;
        }
    }
    else   // if the structure already exists, make sure it is good
    {
        sizeBuffer = LocalSize((HLOCAL) lpBuffer);

        if (sizeBuffer == 0)   // bad pointer?
            return NULL;

        // Was the buffer big enough for the structure?
        if (sizeBuffer < sizeBufferMinimum)
        {
            LocalFree(lpBuffer);
            return CheckAndReAllocBuffer(NULL, sizeBufferMinimum);
        }

    }
    // Set the dwTotalSize field to the size of the buffer or the call will     
    // fail.
memset(lpBuffer, 0, sizeBuffer);
   ((LPVARSTRING) lpBuffer ) -> dwTotalSize = (DWORD) sizeBuffer;
    return lpBuffer;
}

Another task that must be accomplished before actually obtaining the line is to call the lineNegotiateAPIVersion function. This function indicates the version of TAPI that the application was written to support. In the DIALIT sample, I pass in constants that are defined at the top of the TAPIUTILS.CPP file to indicate which versions of TAPI support my sample. This is done in the GetDeviceLine member function, which obtains the first usable voice line available to me.

// TAPI version that this sample is designed to use.
#define SAMPLE_TAPI_VERSION 0x00010004

// Early TAPI version.
#define EARLY_TAPI_VERSION 0x00010003

LPLINEDEVCAPS CTapiConnection::GetDeviceLine(DWORD *pdwAPIVersion, 
   LPLINEDEVCAPS lpLineDevCaps)
{
    DWORD dwDeviceID;
    char szLineUnavail[] = "Line Unavailable";
    char szLineUnnamed[] = "Line Unnamed";
    char szLineNameEmpty[] = "Line Name is Empty";
    LPSTR lpszLineName;
    long lReturn;
    char buf[MAX_PATH];
    LINEEXTENSIONID lineExtID;
    BOOL bDone = FALSE;

    for (dwDeviceID = 0; (dwDeviceID < m_dwNumDevs) && !bDone; dwDeviceID ++)
    {

        lReturn = ::lineNegotiateAPIVersion(m_hLineApp, dwDeviceID, 
            EARLY_TAPI_VERSION, SAMPLE_TAPI_VERSION,
            pdwAPIVersion, &lineExtID);

        if ((HandleLineErr(lReturn))&&(*pdwAPIVersion))
        {
            lpLineDevCaps = MylineGetDevCaps(lpLineDevCaps, dwDeviceID,
               *pdwAPIVersion);

            if ((lpLineDevCaps -> dwLineNameSize) &&
                (lpLineDevCaps -> dwLineNameOffset) &&
                (lpLineDevCaps -> dwStringFormat == STRINGFORMAT_ASCII))
            {
                // This is the name of the device.
                lpszLineName = ((char *) lpLineDevCaps) + 
                    lpLineDevCaps -> dwLineNameOffset;
                sprintf(buf, "Name of device is: %s\n", lpszLineName);
                OutputDebugString(buf);

            }
            else  // DevCaps doesn't have a valid line name. Unnamed.
                lpszLineName = szLineUnnamed;
        }
        else   // Couldn't NegotiateAPIVersion. Line is unavail.
            lpszLineName = szLineUnavail;

        // If this line is usable and we don't have a default initial
        // line yet, make this the initial line.
        if ((lpszLineName != szLineUnavail) && 
            (lReturn == SUCCESS )) 
        {
            m_dwDeviceID = dwDeviceID;
            bDone = TRUE;
        }
        else 
            m_dwDeviceID = MAXDWORD;
    }
    return (lpLineDevCaps);
}

In your application, you may wish to get the line information and present it to the user. For example, on my laptop computer I have two modems: one that I use when I am running without using the docking station and one that I use when I am docked. The way my sample is written now, the first modem selected is the one that I use when I am docked. This means that I can't test the sample at home. (Too bad. How sad.)

Calling lineOpen

We've chosen a line, and now we want to know whether this is the line we want to use. In the DIALIT sample, we check to see if the line is usable, if it can handle voice calls, and if it is capable of dialing out. This is done by checking the values filled into the LINEDEVCAPS structure I passed in.

if (!(lpLineDevCaps->dwBearerModes & LINEBEARERMODE_VOICE ))
{
    AfxMessageBox("The selected line doesn't support VOICE capabilities");
    goto DeleteBuffers;
}
// Does this line have the capability to make calls?
if (!(lpLineDevCaps->dwLineFeatures & LINEFEATURE_MAKECALL))
{
    AfxMessageBox("The selected line doesn't support MAKECALL capabilities");
    goto DeleteBuffers;
}
// Does this line have the capability for interactive voice?
if (!(lpLineDevCaps->dwMediaModes & LINEMEDIAMODE_INTERACTIVEVOICE))
{
    AfxMessageBox("The selected line doesn't support INTERACTIVE VOICE
      capabilities");
    goto DeleteBuffers;
}

An application uses the lineOpen function to place calls and to monitor incoming calls. When opening a line for an outgoing call, the DIALIT sample sets the privilege level to LINECALLPRIVILEGE_NONE to insulate it from incoming calls and to allow outgoing calls. You can set other privileges, but these are for call monitoring.

// Open the line for an outgoing call.
do
{
    lReturn = ::lineOpen(m_hLineApp, m_dwDeviceID, &m_hLine,
        m_dwAPIVersion, 0, 0, LINECALLPRIVILEGE_NONE, 0, 0);

    if((lReturn == LINEERR_ALLOCATED) ||(lReturn == LINEERR_RESOURCEUNAVAIL))
    {
        HangupCall();
        OutputDebugString("Line is already in use by a non-TAPI application"
                " or by another TAPI Service Provider.\n");
        goto DeleteBuffers;
    }

    if (HandleLineErr(lReturn))
        continue;
    else
    {
        OutputDebugString("Unable to Use Line\n");
        HangupCall();
        goto DeleteBuffers;
    }
}
while(lReturn != SUCCESS);

Step Three: Placing the Call

That's right. We haven't even placed the call yet. Aren't you glad I've written a class?

An application uses the lineMakeCall function to place a call. This function takes the following parameters:

The lineMakeCall function returns a positive "request ID" (which I saved in the m_dwRequestID member variable of my class) if the function will be completed asynchronously, or a negative error number if an error has occurred. The following function demonstrates how the LPCALLPARAMS structure can be filled in to support a simple interactive voice call.

LPLINECALLPARAMS CTapiConnection::CreateCallParams (
    LPLINECALLPARAMS lpCallParams, LPCSTR lpszDisplayableAddress)
{
    size_t sizeDisplayableAddress;

    if (lpszDisplayableAddress == NULL)
        lpszDisplayableAddress = "";

    sizeDisplayableAddress = strlen(lpszDisplayableAddress) + 1;

    lpCallParams = (LPLINECALLPARAMS) CheckAndReAllocBuffer(
        (LPVOID) lpCallParams, 
        sizeof(LINECALLPARAMS) + sizeDisplayableAddress);

    if (lpCallParams == NULL)
        return NULL;
    
// This is where we configure the line.
    lpCallParams -> dwBearerMode = LINEBEARERMODE_VOICE;
    lpCallParams -> dwMediaMode  = LINEMEDIAMODE_INTERACTIVEVOICE;

    // This specifies that we want to use only IDLE calls and
    // don't want to cut into a call that might not be IDLE (ie, in use).
    lpCallParams -> dwCallParamFlags = LINECALLPARAMFLAGS_IDLE;

    // If there are multiple addresses on line, use first anyway.
    // It will take a more complex application than a simple tty app
    // to use multiple addresses on a line anyway.
    lpCallParams -> dwAddressMode = LINEADDRESSMODE_ADDRESSID;

    // Address we are dialing.
    lpCallParams -> dwDisplayableAddressOffset = sizeof(LINECALLPARAMS);
    lpCallParams -> dwDisplayableAddressSize = sizeDisplayableAddress;
    strcpy((LPSTR)lpCallParams + sizeof(LINECALLPARAMS), 
      lpszDisplayableAddress);

    return lpCallParams;
}

After the lineMakeCall function successfully sets up the call, the application receives a LINE_REPLY message (the asynchronous reply to lineMakeCall). The application gets this message through its callback function. This means that a call at the local end has been established (that is, we have a dial tone). The LINE_REPLY message also informs the application that the call handle returned by lineMakeCall is valid. The following code shows the line callback function that the DIALIT sample uses.

void CALLBACK CTapiConnection::lineCallbackFunc(
    DWORD dwDevice, DWORD dwMsg, DWORD dwCallbackInstance, 
    DWORD dwParam1, DWORD dwParam2, DWORD dwParam3)
{

    // Handle the line messages.
    switch(dwMsg)
    {
        case LINE_CALLSTATE:
            MyThis->HandleLineCallState(dwDevice, dwMsg, dwCallbackInstance, 
                dwParam1, dwParam2, dwParam3);
            break;

        case LINE_CLOSE:
            // Line has been shut down.
            ASSERT(MyThis);
            MyThis->m_hLine = NULL;
            MyThis->m_hCall = NULL;
            MyThis->HangupCall();   // all handles invalidated by this time
            break;

        case LINE_REPLY:
            if ((long) dwParam2 != SUCCESS)
                OutputDebugString("LINE_REPLY error\n");
            else
                OutputDebugString("LINE_REPLY: successfully replied\n");
            break;

        case LINE_CREATE:
            ASSERT(MyThis);
            if (MyThis->m_dwNumDevs <= dwParam1)
                MyThis->m_dwNumDevs = dwParam1+1;
            break;

        default:
            OutputDebugString("lineCallbackFunc message ignored\n");
            break;
    }
    return;
}

Several other messages or notifications are sent to the application's callback function. For instance, as the call is placed, the call passes through a number of states, each of which results in a LINE_CALLSTATE message sent to the callback function. After the message indicating the connected state is received, the application can begin sending data. I used a handler for the LINE_CALLSTATE message that prints debug messages indicating the current call status. For your application, you may wish to write these messages to a status bar.

void CTapiConnection::HandleLineCallState(
    DWORD dwDevice, DWORD dwMessage, DWORD dwCallbackInstance,
    DWORD dwParam1, DWORD dwParam2, DWORD dwParam3)
{

    // Error if this CALLSTATE doesn't apply to our call in progress.
    if ((HCALL) dwDevice != m_hCall)
    {
        OutputDebugString("LINE_CALLSTATE: Unknown device ID");
        return;
    }

    // dwParam1 is the specific CALLSTATE change that is occurring.
    switch (dwParam1)
    {
        case LINECALLSTATE_DIALTONE:
            OutputDebugString("Dial Tone\n");
            break;

        case LINECALLSTATE_DIALING:
            OutputDebugString("Dialing\n");
            break;

        case LINECALLSTATE_PROCEEDING:
            OutputDebugString("Proceeding\n");
            break;

        case LINECALLSTATE_RINGBACK:
            OutputDebugString("RingBack\n");
            break;

        case LINECALLSTATE_BUSY:
            OutputDebugString("Line busy, shutting down\n");
            HangupCall();
            break;

        case LINECALLSTATE_IDLE:
            OutputDebugString("Line idle\n");
            HangupCall();
            break;

        case LINECALLSTATE_SPECIALINFO:
            OutputDebugString("Special Info, probably couldn't dial number\n");
            HangupCall();
            break;

        case LINECALLSTATE_DISCONNECTED:
        {
            LPSTR pszReasonDisconnected;

            switch (dwParam2)
            {
                case LINEDISCONNECTMODE_NORMAL:
                    pszReasonDisconnected = "Remote Party Disconnected";
                    break;

                case LINEDISCONNECTMODE_UNKNOWN:
                    pszReasonDisconnected = "Disconnected: Unknown reason";
                    break;

                case LINEDISCONNECTMODE_REJECT:
                    pszReasonDisconnected = "Remote Party rejected call";
                    break;

                case LINEDISCONNECTMODE_PICKUP:
                    pszReasonDisconnected = 
                        "Disconnected: Call was picked up on another phone";
                    break;

                case LINEDISCONNECTMODE_FORWARDED:
                    pszReasonDisconnected = "Disconnected: Forwarded";
                    break;

                case LINEDISCONNECTMODE_BUSY:
                    pszReasonDisconnected = "Disconnected: Busy";
                    break;

                case LINEDISCONNECTMODE_NOANSWER:
                    pszReasonDisconnected = "Disconnected: No Answer";
                    break;

                case LINEDISCONNECTMODE_BADADDRESS:
                    pszReasonDisconnected = "Disconnected: Bad Address";
                    break;

                case LINEDISCONNECTMODE_UNREACHABLE:
                    pszReasonDisconnected = "Disconnected: Unreachable";
                    break;

                case LINEDISCONNECTMODE_CONGESTION:
                    pszReasonDisconnected = "Disconnected: Congestion";
                    break;

                case LINEDISCONNECTMODE_INCOMPATIBLE:
                    pszReasonDisconnected = "Disconnected: Incompatible";
                    break;

                case LINEDISCONNECTMODE_UNAVAIL:
                    pszReasonDisconnected = "Disconnected: Unavailable";
                    break;

                case LINEDISCONNECTMODE_NODIALTONE:
                    pszReasonDisconnected = "Disconnected: No Dial Tone";
                    break;

                default:
                    pszReasonDisconnected = 
                        "Disconnected: LINECALLSTATE; Bad Reason";
                    break;

            }

            OutputDebugString(pszReasonDisconnected);
            OutputDebugString("\n");
            HangupCall();
            break;
        }

        case LINECALLSTATE_CONNECTED:   // CONNECTED!!!
            OutputDebugString("Connected!\n");
            break;

        default:
            OutputDebugString("Unhandled LINECALLSTATE message\n");
            break;
    }
}

Step Four: Sending Data

If the DIALIT sample supported data transmission, now would be the time that it would send the data. The user would specify, through some user interface widget, what file or data to send and then initiate the data transmission. The TAPI functions continue to manage the opened line and the call in progress, but the actual transmission is started and controlled by non-TAPI functions (that is, COMM functions). This is the type of transmission that the TAPICOMM sample demonstrates.

In our case, we are establishing a voice call. TAPI continues to monitor the line and call, but if there is anything special we need to do, it is up to us. When a call is connected, the service provider may display a dialog box asking the user to lift the telephone receiver and click the Talk button.

Figure 2. A Call Status dialog box

In our sample, we use a function (borrowed from the TAPICOMM sample) that allows us to resynchronize a TAPI line call by waiting for the LINE_REPLY message. In other words, it waits until a LINE_REPLY is received or the line is shut down. The function is called from the same thread that made the call to lineInitialize.

Note   If you try to call this function from a different thread than the thread that called the lineInitialize function, the PeekMessage function will not be synchronized with the correct thread. Instead, it will be peeking at the message pump for the wrong thread. You will remember from reading the "TAP into the Future" article that TAPI is designed such that a hidden window is created by TAPI on behalf of the application when the lineInitialize function is called. All notifications and messages are sent to the callback function specified in the lineInitialize function. Thus the thread that called lineInitialize contains the message pump for the messages that will be sent to the callback function.

If the call was dropped while in a wait state, this function can potentially be re-entered. We handle this by dropping out of the function and assuming that the call was canceled. This is the reason for the bReentered flag. This flag is set to FALSE when the function is entered and TRUE when the function is exited. If bReentered is ever TRUE during the function processing, then the function was re-entered.

long CTapiConnection::WaitForReply (long lRequestID)
{
    static BOOL bReentered;
    bReentered = FALSE;

    if (lRequestID > SUCCESS)
    {
        MSG msg; 
        DWORD dwTimeStarted;

        m_bReplyReceived = FALSE;
        m_dwRequestedID = (DWORD) lRequestID;

        // Initializing this just in case there is a bug
        // that sets m_bReplyReceived without setting the reply value.
        m_lAsyncReply = LINEERR_OPERATIONFAILED;

        dwTimeStarted = GetTickCount();

        while(!m_bReplyReceived)
        {
            if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }

            // This should only occur if the line is shut down while waiting.
            if ((m_hCall != NULL) &&(!m_bTapiInUse || bReentered))
            {
                bReentered = TRUE;
                return WAITERR_WAITABORTED;
            }

            // It's a really bad idea to timeout a wait for a LINE_REPLY.
            // If we are expecting a LINE_REPLY, we should wait till we get
            // it; it might take a long time to dial (for example).

            // If 5 seconds go by without a reply, it might be a good idea
            // to display a dialog box to tell the user that a
            // wait is in progress and to give the user the capability to
            // abort the wait.
        }

        bReentered = TRUE;
        return m_lAsyncReply;
    }

    bReentered = TRUE;
    return lRequestID;
}

Step Five: Ending the Call

When the user finishes the phone call, the application receives a LINE_CALLSTATE message telling it that the state of a line device has changed. At this point the application can disconnect the call. In the DIALIT sample, the application disconnects the call when the user clicks the Hang Up button. The sample ends the calls, closes the line, shuts down TAPI, and exits.

Before the application disconnects the call, it checks to see if a call is already in progress. If not, the application calls the lineDrop function to place the call in the IDLE state. The call handle must then be released for the finished call. This is done by the lineDeallocateCall function. Finally, lineClose is called to close the line. At this point, there will be no more incoming or outgoing calls using that line handle.

BOOL CTapiConnection::HangupCall()
{
    LPLINECALLSTATUS pLineCallStatus = NULL;
    long lReturn;

    // Prevent HangupCall re-entrancy problems.
    if (m_bStoppingCall)
        return TRUE;

    // If TAPI is not being used right now, then the call is hung up.
    if (!m_bTapiInUse)
        return TRUE;

    m_bStoppingCall = TRUE;

    // If there is a call in progress, drop and deallocate it.
    if (m_hCall)
    {
        pLineCallStatus = (LPLINECALLSTATUS)malloc(sizeof(LINECALLSTATUS));

        if (!pLineCallStatus)
        {
            ShutdownTAPI();
            m_bStoppingCall = FALSE;
            return FALSE;
        }

        lReturn = ::lineGetCallStatus(m_hCall, pLineCallStatus);

        // Only drop the call when the line is not IDLE.
        if (!((pLineCallStatus -> dwCallState) & LINECALLSTATE_IDLE))
            ::lineDrop(m_hCall, NULL, 0);

        // The call is now idle. Deallocate it!
        do
        {
            lReturn = ::lineDeallocateCall(m_hCall);
            if (HandleLineErr(lReturn))
                continue;
            else
            {
                OutputDebugString("lineDeallocateCall unhandled error\n");
                break;
            }
        }
        while(lReturn != SUCCESS);
    }

    // If we have a line open, close it.
    if (m_hLine)
    {
        do
        {
            lReturn = ::lineClose(m_hLine);
            if (HandleLineErr(lReturn))
                continue;
            else
            {
                OutputDebugString("lineClose unhandled error\n");
                break;
            }
        }
        while(lReturn != SUCCESS);
    }

    // Clean up.
    m_hCall = NULL;
    m_hLine = NULL;
    m_bTapiInUse = FALSE;
    m_bStoppingCall = FALSE;   // allow HangupCall to be called again

    // Need to free buffer returned from lineGetCallStatus.
    if (pLineCallStatus)
        free(pLineCallStatus);  
        
    return TRUE;
}

Next, the application calls lineShutdown to end the use of TAPI.

BOOL CTapiConnection::ShutdownTAPI()
{
    long lReturn;

    // If we aren't initialized, then Shutdown is unnecessary.
    if (m_hLineApp == NULL)
        return TRUE;

    // Prevent ShutdownTAPI re-entrancy problems.
    if (m_bShuttingDown)
        return TRUE;

    m_bShuttingDown = TRUE;

    HangupCall();

    lReturn = ::lineShutdown(m_hLineApp);
    if (!HandleLineErr(lReturn))
        OutputDebugString("lineShutdown unhandled error\n");

    m_bTapiInUse = FALSE;
    m_hLineApp = NULL;
    m_hCall = NULL;
    m_hLine = NULL;
    m_bShuttingDown = FALSE;
    return TRUE;
}

At this point the application is finished using TAPI and can continue to do whatever else it was designed to do. In the DIALIT sample, the application exits.

Summary

As mentioned at the beginning of this article, this class was written to provide the most basic TAPI functionality of initialization, ability to obtain a line, dial voice calls, drop a line, and shut down. It can easily be expanded to support data modem capabilities or a snazzy user interface (you can certainly do better than the one I did). The class could even be wrapped into an OLE Control and used by a Visual Basic® application. So now that you have a simple class that you can use to include TAPI functionality in your application, some sample code, the Win32 SDK, the documentation, and this article, you really have no excuse to avoid TAPI any more. I've done much of the hard work for you already, so please make the time spent worthwhile and use the class.