Creating a custom Phone App on Windows Mobile (Part 1) - Using TAPI (
Page 3 of 5 )
Using TAPI
The phone class is just a wrapper into the existing phone software. If you really want some phone capabilities right in your program, you need to use several other functions built into the Telphony API, or TAPI.
ADVERTISEMENT
This API is huge. I can’t possibly cover it all here. If you explore the documentation and loads of sites out there about it, you’ll see just how much you can do with it. For this article, we’ll just touch on the basics.
And please note: I took the liberty of simplifying some of the calls. Some calls take optional (and huge) structures that you can fill in for additional phone features. I am using some of these here, but for many of them I’m just passing a null pointer since they’re optional. And to simplify that, I made some custom versions of the functions that accept an integer (so I can pass 0, which represents null), instead of the actual structure. If you want to use the structures, you’ll have to look at the API online and create the structures yourself. (If there’s enough interest, I might create the additional structures and write another article on it.) (In the code that I’m providing, I notate such skipped places with “pass 0, see text”.)
Marshaling the TAPI functions
Unfortunately, .NET wrappers aren’t built in for the functions I need. But that’s not a huge problem; to access native functions we can create wrapper functions using a technique called marshaling. This isn’t as complicated as some people make it out to be. (Although there are many advanced features in marshaling that are complex; but getting up and running quickly is easy.) If you want to call a native function, you simply use an attribute (which looks like a class constructor in brackets) that states that a function is a native function, and then you provide what’s essentially a description of the function. Here’s an example:
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineShutdown(IntPtr apphandle);
The attribute is the first line. DllImport is a class (actually, the full class name is DllImportAttribute, but you leave off the Attribute part of the name), and the items in parentheses are parameters to the constructor. The first parameter says that the actual function we need is inside the Coredll.dll file. The second parameter is for if we want to capture Win32 errors. (I’m not using them in this short example, but the full program later on will need them. That’s why I’m including that parameter here.)
We need to create a set of lines like this for each TAPI function we need. Since C# is fully object-oriented, we can’t have just standalone functions; they must be in a class. So we’ll create a class to hold the functions. But the functions are static, so they’re easy to call without having to create an instance of the class.
Additionally, we need to create a few structures that are needed for these classes. As I mentioned earlier, I only created a couple such structures. (One structure is also a bit of a hack; it’s a structure to hold a Guid. Such structures already exist in .NET, but I wanted a very short simple one that can be passed into the functions.)
I’m putting all these structures and classes in their own code file. So let’s do that. Add a new Class (.cs) file to your project. Call it MobileTapi.cs. When the file is created, remove the MobileTapi class that Visual Studio created automatically, and rename the namespace to MobileTapi like so:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace MobileTapi
{
}
(I do this sort of thing when I have small namespaces all include in a single file with more than one class. That way the filename matches the namespace name!)
In one large shot, here’s the code. Since this article focuses on using TAPI and not how to write marshaling code, I’m going to forgo any more discussion on how this code works from the marshaling standpoint. Instead I’ll describe what the functions do.
Also, I should point out that this isn’t exactly my idea of an idea wrapper class. The class simply provides access to the direct API functions, and using them requires filling in structures manually and passing around what are called handles (which are very familiar to the C++/Win32 programmers out there). In my opinion, a well-designed class should abstract such details away so you don’t have to deal with thme. This one doesn’t, though. Hopefully one day I’ll enhance it to do so, making a good, general-purpose, easy-to-use class. For now, bear with me.
Here’s the complete code for the MobileTapi.cs file. (Remember to add the line at the top for using the Interop services.)
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices; // add this
namespace MobileTapi
{
[StructLayout(LayoutKind.Sequential)]
public struct LineExtensionId
{
public int id0, id1, id2, id3;
}
[StructLayout(LayoutKind.Sequential)]
public struct LineGuid
{
public int id0, id1, id2, id3;
}
[StructLayout(LayoutKind.Sequential)]
public struct LineDialParams
{
public int dwDialPause;
public int dwDialSpeed;
public int dwDigitDuration;
public int dwWaitForDialtone;
}
public struct LineCaps
{
public int dwTotalSize;
public int dwNeededSize;
public int dwUsedSize;
public int dwProviderInfoSize;
public int dwProviderInfoOffset;
public int dwSwitchInfoSize;
public int dwSwitchInfoOffset;
public int dwPermanentLineID;
public int dwLineNameSize;
public int dwLineNameOffset;
public int dwStringFormat;
public int dwAddressModes;
public int dwNumAddresses;
public int dwBearerModes;
public int dwMaxRate;
public int dwMediaModes;
public int dwGenerateToneModes;
public int dwGenerateToneMaxNumFreq;
public int dwGenerateDigitModes;
public int dwMonitorToneMaxNumFreq;
public int dwMonitorToneMaxNumEntries;
public int dwMonitorDigitModes;
public int dwGatherDigitsMintimeout;
public int dwGatherDigitsMaxTimeout;
public int dwMedCtlDigitMaxListSize;
public int dwMedCtlMediaMaxListSize;
public int dwMedCtlToneMaxListSize;
public int dwMedCtlCallStateMaxListSize;
public int dwDevCapFlags;
public int dwMaxNumActiveCalls;
public int dwAnswerMode;
public int dwRingModes;
public int dwLineStates;
public int dwUUIAcceptSize;
public int dwUUIAnswerSize;
public int dwUUIMakeCallSize;
public int dwUUIDropSize;
public int dwUUISendUserUserInfoSize;
public int dwUUICallInfoSize;
public LineDialParams MinDialParams;
public LineDialParams MaxDialParams;
public LineDialParams DefaultDialParams;
public int dwNumTerminals;
public int dwTerminalCapsSize;
public int dwTerminalCapsOffset;
public int dwTerminalTextEntrySize;
public int dwTerminalTextSize;
public int dwTerminalTextOffset;
public int dwDevSpecificSize;
public int dwDevSpecificOffset;
public int dwLineFeatures;
public int dwSettableDevStatus;
public int dwDeviceClassesSize;
public int dwDeviceClassesOffset;
public LineGuid PermanentLineGuid;
public int dwAddressTypes;
public LineGuid ProtocolGuid;
public int dwAvailableTracking;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 500)]
public char[] extra;
public string getProviderInfo() {
List<char> extraList = extra.ToList<char>();
return new String(extraList.GetRange((dwProviderInfoOffset - 292) / 2,
(dwProviderInfoSize / 2) - 1).ToArray());
}
public string getSwitchInfo() {
List<char> extraList = extra.ToList<char>();
return dwSwitchInfoSize < 1 ? "no switchInfo" :
new String(extraList.GetRange((dwSwitchInfoOffset - 292) / 2,
(dwSwitchInfoSize / 2) - 1).ToArray());
}
public string getLineName() {
List<char> extraList = extra.ToList<char>();
return dwLineNameSize < 1 ? "no lineName" :
new String(extraList.GetRange((dwLineNameOffset - 292) / 2,
(dwLineNameSize / 2) - 1).ToArray());
}
public string getTerminalCaps() {
List<char> extraList = extra.ToList<char>();
return dwTerminalCapsSize < 1 ? "no terminalCaps" :
new String(extraList.GetRange((dwTerminalCapsOffset - 292) / 2,
(dwTerminalCapsSize / 2) - 1).ToArray());
}
public string getTerminalText() {
List<char> extraList = extra.ToList<char>();
return dwTerminalTextSize < 1 ? "no terminalText" :
new String(extraList.GetRange((dwTerminalTextOffset - 292) / 2,
(dwTerminalTextSize / 2) - 1).ToArray());
}
public string getDevSpecific() {
List<char> extraList = extra.ToList<char>();
if (dwDevSpecificSize > 0) {
// Don't subtract 1 from size since this isn't a string
List<char> range = extraList.GetRange((dwDevSpecificOffset - 292) / 2,
(dwDevSpecificSize / 2));
StringBuilder builder = new StringBuilder();
foreach (byte c in range) {
byte[] bytes = BitConverter.GetBytes(c);
foreach (byte b in bytes) {
builder.Append(b.ToString("X"));
}
}
return builder.ToString();
}
else {
return "";
}
}
public string getDeviceClasses() {
List<char> extraList = extra.ToList<char>();
List<char> test;
return dwDeviceClassesSize < 1 ? "no deviceClasses" :
new String(extraList.GetRange((dwDeviceClassesOffset - 292) / 2,
(dwDeviceClassesSize / 2) - 1).ToArray()).Replace("\0", ";");
}
public string getMediaModeAsHex() {
return dwMediaModes.ToString("X8");
}
}
public class TapiFunctions
{
public delegate void lineCallbackFunc(int device, int Msg,
int callbackInst, int param1, int param2, int param3);
public const int LINEMEDIAMODE_INTERACTIVEVOICE = 4;
// represents voice-call capabilities
[DllImport("coredll.dll")]
// passing int -- see text
public static extern IntPtr GetModuleHandle(int lpModuleName);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineInitialize(out IntPtr apphandle,
IntPtr dllhandle, lineCallbackFunc callbackfunction, string appname,
out int numdevs);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineNegotiateAPIVersion(IntPtr apphandle, int deviceId,
int lowVersion, int highVersion, out int version, ref LineExtensionId extid);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineGetDevCaps(IntPtr apphandle, int devid, int apiversion,
int extversion, ref LineCaps caps);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineOpen(IntPtr apphandle, int devid, out IntPtr hline,
int apiversion, int extversion,
int callbackdata, int privs, int mediamodes, int callparams /* pass 0, see text */);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineMakeCall(IntPtr apphandle, ref IntPtr pcallhandle,
string number, int country, int lpCallParams /*pass 0, see text */);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineDrop(IntPtr callhandle,
int userinfo /* pass 0, see text */, int size /* pass 0, see text */);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineClose(int handle, string number, int country);
[DllImport("Coredll.dll", SetLastError = true)]
public static extern int lineShutdown(IntPtr apphandle);
}
}
This file defines a few structures (and adds some helper functions to one of them). Notice the line:
[StructLayout(LayoutKind.Sequential)]
This is required for the structures to work right with marshaling. If you’re not familiar with it, you can just use it like I have it; or you can read more about it in the MSDN docs and other places online.
The functions I’m including are:
GetModuleHandle: You need your program’s module handle to initialize the TAPI. The result of this function is passed as a parameter to the following function, lineInitialize.
lineInitialize: This function initializes TAPI and its line system. (A line is an available network connection. You likely have several on yours, including one for voice, probably one for bluetooth, likely a modem, and so on.) This function also tells you how many lines there are.
lineNegotiateAPIVersion: This determines which TAPI version is available for your device.
lineGetDevCaps: Call this function to find out what capabilities a line has (such as voice, or bluetooth, etc.). In our application, we want the one for voice. This function also requires the LineCaps, which was a massive structure that, unfortunately, wasn’t optional. Thus I had to port the huge thing to C#. (Thank goodness for regular expressions and search-and-replace. That made the conversion to C# easier).
lineOpen: When you find the line you want (in our case, the voice one), call this to open the line. This doesn’t actually make the phone call; instead, it opens the line and readies it for a call.
lineMakeCall: Next, call this function to actually make the phone call. You pass the phone number.
lineDrop: To hang up the call, call this function.
lineClose: After hanging up, call this to close the line so it’s available again for later.
lineShutdown: Finally, every call to lineInitialize needs a corresponding call to lineShutdown. (I had some strange stuff happening when early versions of my program crashed and this function wasn’t getting called. For example, instead of making the call, my program would launch the built-in phone app just as if I were using the Phone class like the earlier example.)
As I mentioned, ideally you would be presented with a class that abstracts away these functions. But for now you can use them as I am. Fortunately, even as-is, the functions are pretty straightforward to use, as I’ll show next.
In case you’re curious, the function prototypes and structures for the actual TAPI code are given in a C header file called Tapi.h. In my installation the file is found here:
C:\Program Files\Windows Mobile 6 SDK\PocketPC\Include\Armv4i\Tapi.h