Introduction
The code presented below shows you how to wrap the Native Windows Mobile 6 API in C# so that it can be used with the .NET Compact Framework and targeted applications. Specifically, it shows an example of wrapping the necessary structures and functions to be able to retrieve the call history. The call history is then stored in a database for viewing.
Once again, I've decided to go with a SQL Anywhere 10 database. Mostly, this is because it is really small in footprint and has an integrated web server. Basically, the example below lets you view your call history from Internet Explorer. You can read up on their product at: http://www.sybase.com/products/databasemanagement/sqlanywhere.
Background
While Windows Mobile 6 does support the .NET Compact Framework, a lot of phone specific functions are only available through Native Win32 APIs. See Microsoft's reference at: http://msdn2.microsoft.com/en-us/library/bb416430.aspx. This gap can be bridged by using C# and .NET's interoperability.
Once able to access the call history, it would be nice to actually be able to do something with it. I've always had a craving to have my cake and eat it too. So, what better way to make use of the data than to expose it as a Web Service that can be consumed in any way you like.
The Native C++ Win32 Phone API
First, you'll want to take a look at the specs for the library. I'm only really interested in accessing the call history, so I'll only talk about those parts.
Phone API Functions
HRESULT PhoneOpenCallLog(HANDLE * ph);
HRESULT PhoneCloseCallLog(HANDLE h);
HRESULT PhoneSeekCallLog(HANDLE h, CALLLOGSEEK seek,
DWORD iRecord, LPDWORD piRecord);
HRESULT PhoneGetCallLogEntry(HANDLE h, PCALLLOGENTRY pentry);
Phone API Structures
typedef struct
{
DWORD cbSize;
FILETIME ftStartTime;
FILETIME ftEndTime;
IOM iom;
BOOL fOutgoing:1;
BOOL fConnected:1;
BOOL fEnded:1;
BOOL fRoam:1;
CALLERIDTYPE cidt;
PTSTR pszNumber;
PTSTR pszName;
PTSTR pszNameType;
PTSTR pszNote;
} CALLLOGENTRY, * PCALLLOGENTRY;
Phone API Enumerations
typedef enum
{
CALLERIDTYPE_UNAVAILABLE,
CALLERIDTYPE_BLOCKED,
CALLERIDTYPE_AVAILABLE
} CALLERIDTYPE;
typedef enum
{
CALLLOGSEEK_BEGINNING = 2,
CALLLOGSEEK_END = 4
} CALLLOGSEEK;
typedef enum
{
IOM_MISSED,
IOM_INCOMING,
IOM_OUTGOING
} IOM;
Wrapping the Native API with C# .NET 2.0
Wrapping the Functions
Now, you'll need to prototype the functions in C# and instruct the compiler to pick up the functions from a Native DLL. This is the easiest of all the tasks, but there are a few tricks to be picked up on here. You'll notice below that we don't use pointers, but instead use keywords like out
and ref
in the functions.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public static class CallLog
{
[DllImport("Phone.dll")]
private static extern IntPtr PhoneOpenCallLog(out IntPtr ph);
[DllImport("Phone.dll")]
private static extern IntPtr PhoneCloseCallLog(IntPtr h);
[DllImport("Phone.dll")]
private static extern IntPtr PhoneGetCallLogEntry(IntPtr h, ref CALLLOGENTRY pentry);
[DllImport("Phone.dll")]
private static extern IntPtr PhoneSeekCallLog(IntPtr hdb,
CALLLOGSEEK seek, uint iRecord, ref uint piRecord);
}
}
}
The HRESULT
typdef is compatible with IntPtr
, though HRESULT
is unsigned and IntPtr
is signed.
Notice the use of out
when we expect the function to fill in the value of a pointer, and ref
when we expect it to fill in a structure or variable.
Wrapping the Structures
Now, we'll need to create compatible classes that can be filled in by the Native DLL, but used by our C# application.
This is done in two ways; first create a compatible class that can be used by C++, and then create a C# class that's cleaner.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public static class CallLog
{
[StructLayout(LayoutKind.Explicit, Size = 48)]
private struct CALLLOGENTRY
{
[FieldOffset(0)]
public uint cbSize;
[FieldOffset(4)]
public long ftStartTime;
[FieldOffset(12)]
public long ftEndTime;
[FieldOffset(20)]
public IOM iom;
[FieldOffset(24)]
public byte flags;
[FieldOffset(28)]
public CALLERIDTYPE cidt;
[FieldOffset(32)]
public IntPtr pszNumber;
[FieldOffset(36)]
public IntPtr pszName;
[FieldOffset(40)]
public IntPtr pszNameType;
[FieldOffset(44)]
public IntPtr pszNote;
}
}
}
}
You'll notice the use of the [StructLayout()]
. This is because we need the struct to correspond exactly to what the Native DLL expects (in other words, it needs to be bit-wise compatible). Since the .NET Compact Framework doesn't support all of the StructLayout
s, we have to explicitly tell the compiler where each chunk of data should go.
Next, we can create C# classes that make use of C# types and classes to store more usable forms of data. I've omitted re-writing all the accessors here, but obviously, every member will need an accessor.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public class CallLogEntry
{
internal CallLogEntry() { }
private DateTime _StartTime;
private DateTime _EndTime;
private Boolean _IsOutgoing;
private Boolean _IsConnected;
private Boolean _IsEnded;
private Boolean _IsRoaming;
private CallerIDType _CallerID;
private String _CallerName;
private String _CallerNumber;
public DateTime StartTime
{
get
{
return _StartTime;
}
internal set
{
_StartTime = value;
}
}
public DateTime EndTime
{
get
{
return _EndTime;
}
internal set
{
_EndTime = value;
}
}
public Int32 Duration
{
get
{
return _EndTime.Subtract(_StartTime).Seconds;
}
}
public Boolean IsOutgoing
{
get
{
return _IsOutgoing;
}
internal set
{
_IsOutgoing = value;
}
}
}
}
}
The set
methods of the accessors as well as the constructor are internal
because no one (except us) should be able to actually create or edit call log entries!
Wrapping the Enumerations
So maybe, I lied when I said that the functions were the easiest to wrap. The enumerations are quite simple, you just need to do it twice: once to keep naming and value ordering consistent with the Native DLL, and again to create "cleaner" enumerations. This is a bit of unnecessary overkill, but it's good practice to really separate Native and Managed code.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public class CallLogEntry
{
private enum CALLERIDTYPE
{
CALLERIDTYPE_UNAVAILABLE,
CALLERIDTYPE_BLOCKED,
CALLERIDTYPE_AVAILABLE
}
private enum CALLLOGSEEK
{
CALLLOGSEEK_BEGINNING = 2,
CALLLOGSEEK_END = 4
}
private enum IOM
{
IOM_MISSED,
IOM_INCOMING,
IOM_OUTGOING
}
}
}
}
We won't actually need the CALLLOGSEEK
enumeration in our "clean" enums because it has no use.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public enum CallerIDType
{
Unavailable,
Blocked,
Available
}
public enum Iom
{
Missed,
Incoming,
Outgoing
}
}
}
Wrapping Return Codes
Finally, you should create similarly named return values to check whether the functions are performing correctly or not. You could wrap all of them, but you only need one (to check for success, since anything else is bad).
private const Int64 S_OK = 0x00000000;
Just be sure to use compatible checking:
IntPtr HRESULT = SomeNativeWrappedFunction();
if(HRESULT.ToInt64() != S_OK)
{
throw new Exception("Error!");
}
Putting it All Together
Once all the interfaces have been wrapped, we actually need to do something with them (i.e., retrieve the call history).
Notice the use of the keyword unsafe
. This is because we don't want the Garbage Collector to be moving our structs around since we are using pointers.
using System;
using System.Runtime.InteropServices;
namespace WindowsMobile6
{
namespace Phone
{
public class CallLogEntry
{
public static CallLogEntry[] Entries
{
get
{
CallLogEntry[] entries = new CallLogEntry[0];
unsafe
{
IntPtr result = IntPtr.Zero;
IntPtr log = IntPtr.Zero;
uint lastEntryIndex = 0;
uint currentEntryIndex = 0;
result = PhoneOpenCallLog(out log);
if (result.ToInt64() != S_OK) throw new Exception("Failed to Open Call Log");
result = PhoneSeekCallLog(log, CALLLOGSEEK.CALLLOGSEEK_END, 0,
ref lastEntryIndex);
if (result.ToInt64() != S_OK) throw new Exception("Failed to Seek Call Log");
result = PhoneSeekCallLog(log, CALLLOGSEEK.CALLLOGSEEK_BEGINNING,
0, ref currentEntryIndex);
if (result.ToInt64() != S_OK) throw new Exception("Failed to Seek Call Log");
entries = new CallLogEntry[lastEntryIndex + 1];
for (uint i = 0; i <= lastEntryIndex; i++)
{
CALLLOGENTRY entry = new CALLLOGENTRY();
entry.cbSize = (uint)Marshal.SizeOf(typeof(CALLLOGENTRY));
result = PhoneGetCallLogEntry(log, ref entry);
if (result.ToInt64() != S_OK)
throw new Exception("Failed to Read Call Log Entry");
entries[i] = new CallLogEntry();
entries[i].StartTime = DateTime.FromFileTime(entry.ftStartTime);
entries[i].EndTime = DateTime.FromFileTime(entry.ftEndTime);
entries[i].IsOutgoing = (entry.flags & 0x1) != 0;
entries[i].IsConnected = (entry.flags & 0x2) != 0;
entries[i].IsEnded = (entry.flags & 0x4) != 0;
entries[i].IsRoaming = (entry.flags & 0x8) != 0;
entries[i].CallerID = (CallerIDType)(entry.cidt);
entries[i].CallerName = Marshal.PtrToStringUni(entry.pszName);
entries[i].CallerNumber = Marshal.PtrToStringUni(entry.pszNumber);
}
result = PhoneCloseCallLog(log);
if (result.ToInt64() != S_OK)
throw new Exception("Failed to Close Call Log");
}
return entries;
}
}
}
}
}
I've placed all this code in a C# DLL that can now be used in any .NET application. Now, we can use our Managed Phone Library to accomplish our task of retrieving and storing the call history.
Storing the Data
You'll need a table to store the data, a Stored Procedure to retrieve and format the data, and then a Web Service to expose it. Just use an interactive SQL command executer to run the following scripts.
Create the Users
GRANT CONNECT TO "PHONE";
GRANT GROUP TO "PHONE";
GRANT CONNECT TO "phone_user" IDENTIFIED BY 'sql';
GRANT CONNECT TO "webservice_user" IDENTIFIED BY 'sql';
GRANT MEMBERSHIP IN GROUP "PHONE" TO "phone_user";
GRANT MEMBERSHIP IN GROUP "PHONE" TO "webservice_user";
GRANT SELECT ON "PHONE"."CallHistory" TO "PHONE";
GRANT EXECUTE ON "PHONE"."GetCallHistory" TO "PHONE";
GRANT SELECT, INSERT, DELETE, UPDATE ON "PHONE"."CallHistory" TO "phone_user";
GRANT EXECUTE ON "PHONE"."GetCallHistory" TO "phone_user";
GRANT SELECT ON "PHONE"."CallHistory" TO "webservice_user";
GRANT EXECUTE ON "PHONE"."GetCallHistory" TO "webservice_user";
Creating the Table
CREATE TABLE "PHONE"."CallHistory"
(
"ID" uniqueidentifier NOT NULL DEFAULT newid(*),
"StartTime" "datetime" NOT NULL,
"EndTime" "datetime" NULL,
"IsConnected" bit NOT NULL,
"IsEnded" bit NOT NULL,
"IsOutgoing" bit NOT NULL,
"IsRoaming" bit NOT NULL,
"CallerID" tinyint NOT NULL,
"CallerName" long nvarchar NULL,
"CallerNumber" long nvarchar NULL,
PRIMARY KEY ( "ID" ASC )
);
Creating the Stored Procedure
CREATE PROCEDURE "PHONE"."GetCallHistory"()
RESULT (html_doc XML)
BEGIN
DECLARE datacursor CURSOR FOR SELECT StartTime, EndTime, IsConnected,
IsOutgoing, CallerName,
CallerNumber FROM PHONE.CallHistory
ORDER BY StartTime DESC;
DECLARE html LONG VARCHAR;
DECLARE startTime DATETIME;
DECLARE endTime DATETIME;
DECLARE isConnected BIT;
DECLARE isOutgoing BIT;
DECLARE typeCall NVARCHAR(4);
DECLARE callerName LONG VARCHAR;
DECLARE callerNumber LONG VARCHAR;
SET html = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>My Call History</title>
</head>
<body style="padding-right: 0px; padding-left: 0px;
font-size: 8pt; padding-bottom: 0px;
margin: 0px; color: white;
padding-top: 0px;
font-family: Tahoma, Arial, Sans-Serif;
background-color: #778899">
<table style="font-size: 8pt">
<tr style="font-weight: bold">
<td>Start Time</td>
<td>End Time</td>
<td>Type</td>
<td>Name</td>
<td>Number</td>
</tr>';
CALL dbo.sa_set_http_header( 'Content-Type', 'text/html' );
OPEN datacursor;
lp: LOOP
FETCH NEXT datacursor INTO startTime, endTime, isConnected, isOutgoing, callerName, callerNumber;
IF SQLCODE <> 0 THEN LEAVE lp END IF;
IF isOutgoing = 1 THEN SET typeCall = 'OUT'
ELSEIF isConnected = 0 THEN SET typeCall = 'MISS'
ELSEIF isConnected = 1 THEN SET typeCall = 'IN'
ELSE SET typeCall = 'CALL'
END IF;
SET html = HTML_DECODE( XMLCONCAT( html, '<tr>'
+ '<td>' + DATEFORMAT(startTime, 'YY-MM-DD H:NN AA') + '</td>'
+ '<td>' + DATEFORMAT(endTime, 'YY-MM-DD H:NN AA') + '</td>'
+ '<td>' + typeCall + '</td>'
+ '<td>' + callerName + '</td>'
+ '<td>' + callerNumber + '</td>'
+ '</tr>') );
END LOOP;
CLOSE datacursor;
SET html = HTML_DECODE( XMLCONCAT( html,
'</table>
</body>
</html>') );
SELECT HTML_DECODE( XMLCONCAT(html) );
END
Creating the Web Service
CREATE SERVICE "root" TYPE 'RAW' AUTHORIZATION OFF USER
"webservice_user" AS call PHONE.GetCallHistory();
Using C# .NET 2.0 Classes to Retrieve and Store the Call History in the Database
Everything we've done so far has been to build up to this point. First, we're going to need to reference the WindowsMobile6.Phone Library we just created. Because I'm using SQL Anywhere 10, I have to reference the CE version of their DLL.
using WindowsMobile6.Phone;
using iAnywhere.Data.SQLAnywhere;
So now, retrieving the call history is reduced to a simple property call: CallLogEntry[] entries = CallLog.Entries;
.
SAConnection conn;
SACommand cmd;
string sqlstmt;
DateTime latestEntryTime;
conn = new SAConnection("ENG=callhistory;UID=phone_user;PWD=sql");
conn.Open();
sqlstmt = "SELECT TOP 1 StartTime FROM PHONE.CallHistory ORDER BY StartTime DESC";
cmd = new SACommand(sqlstmt, conn);
if (cmd.ExecuteScalar() != null)
latestEntryTime = (DateTime)cmd.ExecuteScalar();
else
latestEntryTime = DateTime.MinValue;
CallLogEntry[] entries = CallLog.Entries;
for (int i = 0; i < entries.Length; i++)
{
if (entries[i].StartTime > latestEntryTime)
{
sqlstmt = "INSERT INTO PHONE.CallHistory (StartTime, EndTime," +
" IsConnected, IsEnded, IsOutgoing, IsRoaming, " +
"CallerID, CallerName, CallerNumber)";
sqlstmt += " VALUES('";
sqlstmt += entries[i].StartTime.ToString("yyyy-MM-dd hh:mm:ss tt") + "', '";
sqlstmt += entries[i].EndTime.ToString("yyyy-MM-dd hh:mm:ss tt") + "', ";
sqlstmt += ((int)(entries[i].IsConnected ? 1 : 0)).ToString() + ", ";
sqlstmt += ((int)(entries[i].IsEnded ? 1 : 0)).ToString() + ", ";
sqlstmt += ((int)(entries[i].IsOutgoing ? 1 : 0)).ToString() + ", ";
sqlstmt += ((int)(entries[i].IsRoaming ? 1 : 0)).ToString() + ", ";
sqlstmt += (int)entries[i].CallerID + ", '";
sqlstmt += entries[i].CallerName + "', '";
sqlstmt += entries[i].CallerNumber + "')";
cmd = new SACommand(sqlstmt, conn);
cmd.ExecuteNonQuery();
}
}
conn.Close();
Consuming the Web Service (i.e., Viewing the Data)
Because there is a built-in Web Service in the SQL Anywhere database, you just need to open up Internet Explorer.