Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Retrieving and Storing Call History

0.00/5 (No votes)
19 Dec 2007 2  
This article describes how to create a wrapper class for the native Phone API, and then uses it to retrieve and store the call history.

Screenshot

Screenshot

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

// This function opens the call log and sets the seek pointer
// to start searching from the beginning of the log.
HRESULT PhoneOpenCallLog(HANDLE * ph);

// This function closes the call log.
HRESULT PhoneCloseCallLog(HANDLE h);

//This function initiates a search that ends at a given entry in a call log.
HRESULT PhoneSeekCallLog(HANDLE h, CALLLOGSEEK seek, 
                         DWORD iRecord, LPDWORD piRecord);

// This function returns the information for the specified call
// log entry and then advances the seek pointer
// to the next entry in the call log.
HRESULT PhoneGetCallLogEntry(HANDLE h, PCALLLOGENTRY pentry);

Phone API Structures

// The CALLLOGENTRY structure is used to store
// the information for a specific call log entry.
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

// The CALLERIDTYPE enumeration specifies the caller ID type.
typedef enum
{
    CALLERIDTYPE_UNAVAILABLE,
    CALLERIDTYPE_BLOCKED,
    CALLERIDTYPE_AVAILABLE
} CALLERIDTYPE;
      
// The CALLLOGSEEK enumeration specifies the location
// within the call log where a search will begin.
typedef enum
{
    CALLLOGSEEK_BEGINNING = 2,
    CALLLOGSEEK_END = 4
} CALLLOGSEEK;
      
// The IOM enumeration specifies the caller ID type.
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 StructLayouts, 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;
        }
    }
      
    /* ... Continued Accessors ... */
  }
}
}

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();
      
// Notice the .ToInt64()

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));
            //cbSize MUST be set before passing 
            //the struct in to the function! Refer to the doc.

            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);

// We only want to synchronize the database with new entries,
// otherwise we'd get duplicate data.
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here