Code Repo    |     RSS
MD's Technical Sharing



Wednesday, November 5, 2008

Read Write Call History

Objective: Retrieve and modify call history from a Windows Mobile device.

Read-only access

The CallLog API (http://msdn.microsoft.com/en-us/library/ms894745.aspx) provides read-only access to call history via its PhoneOpenCallLog and PhoneGetCallLogEntry function. For a C# implementation via P/Invoke, the OpenNetCF has a nice wrapper via its OpenNETCF.Phone namespace. Refer to OpenNETCF 1.4 for full source code.

Read-write access

The Call Log database is in CEDB/EDB format. If you want to modify it you'll have to do it manually using CEDB or EDB API's. For Windows Mobile 5 and higher, use EDB to open the volume \pim.vol in the clog.db database.

To use EDB, the following declaration

#define EDB

must be added on top on the source code (or stdafx.h for apps that use pre-compiled headers).

This code demonstrates how to modify the call log using EDB API. The necessary steps are as follows:

1. Call history is stored in the EDB database pim.vol. Mount the database file first.

CeMountDBVolEx(&m_ceguidDB, L"\\pim.vol", NULL, OPEN_EXISTING);

2. Open the call history table clog.db. Return INVALID_HANDLE_VALUE if error. CEDB_AUTOINCREMENT makes sure that each call to CeReadRecordPropsEx will move the cursor forward to the next record so that no manual seeking in the database is required

HANDLE m_hDBCallLog = CeOpenDatabaseEx2( &m_ceguidDB, &m_ceoidCallLog, TEXT("clog.db"), 0, CEDB_AUTOINCREMENT, NULL);

3. Seek to the beginning/end of the table. Return non-zero if OK, 0 if error.

CEOID ceOIDTemp = CeSeekDatabase(m_hDBCallLog, CEDB_SEEK_BEGINNING, 0, NULL);

4.1. Read the records repeatedly (forward cursor) until there are no records left. This is the most tricky part as we need to guess the database format, e.g. how many columns each record has and what each column is for. Refer to [3] for a description of possible format. We need to define every fields that the record contains:

//Indentify the column indexes (in the database) of the properties to be retrieved
CEPROPID propReserved1 MAKELONG(CEVT_I2, 1); //Always 1
CEPROPID propStartTime = MAKELONG(CEVT_FILETIME, 2); //call start time
CEPROPID propEndTime = MAKELONG(CEVT_FILETIME, 3); //call end time
CEPROPID propCallFlags = MAKELONG(CEVT_I4, 4); //Identify call property (connected, roaming, incoming, outgoing, missed, etc.), every bit has a certain meaning
CEPROPID propSomeText = MAKELONG(CEVT_LPWSTR, 5); //Some texts to display
CEPROPID propCallNumber = MAKELONG(CEVT_LPWSTR, 6); //called/incoming number
CEPROPID propCallName = MAKELONG(CEVT_LPWSTR, 7); //name from address book
CEPROPID propReserved2 = MAKELONG(CEVT_I4, 8); //always 0
CEPROPID propID = MAKELONG(CEVT_AUTO_I4, 9); //Sequence number, auto-increase
DWORD dwBuf = 0; //returned size of buffer to hold record properties
WORD wProps = 5; //length of property array, e.g. number of properties to receive CEPROPVAL *pPropVals = NULL; //properties of records are returned in this array
CEPROPID propsToRead[5] = {propStartTime, propEndTime, propCallNumber, propCallName, propCallFlags}; //array of property to retrieve

After that, call CeReadRecordPropsEx to read the record. This function returns non-zero if OK, 0 if failed:

CEOID readRecord = CeReadRecordPropsEx(m_hDBCallLog, CEDB_ALLOWREALLOC, &wProps, propsToRead, reinterpret_cast(&pPropVals), &dwBuf, NULL);

To retrieve the read properties,

  1. check pPropVals[i].wFlags to make sure that the field has been read properly
  2. Check LOWORD(pPropVals[i].propid) for the field data type
  3. According to the field data type, retrieve data from pPropVals[i].val

Deleting a record is straightforward once you have its OID:

CeDeleteRecord(m_hDBCallLog, ceOIDTemp);

Creating a new record is possible via CeWriteRecordProps. This function return the ID of the created record if successful, and zero if failed:

CEPROPVAL propsToWrite[5]; //array of properties to write for the current record
WORD length = sizeof(propsToWrite)/sizeof(CEPROPVAL);
CEOID newRec = CeWriteRecordProps(m_hDBCallLog, 0, length, propsToWrite);

Finally, the volume needs to be unmounted:

CeUnmountDBVol(&m_ceguidDB);

Migrating to C# via P/Invoke:

Most functions are available by dllimporting from coredll.dll. Unsafe code in C# needs to be used as CeReadRecordPropsEx works with pointer to byte buffer, which cannot be simulated via the .NET class IntPtr.

[DllImport("coredll.dll", SetLastError=true)]
static unsafe extern int CeReadRecordPropsEx(IntPtr hDbase, int dwFlags, ref int lpcPropID, IntPtr rgPropID, byte** lplpBuffer, ref int lpcbBuffer, IntPtr hHeap);

As in C++, we have to first mount, open the database and seek to the beginning

CeMountDBVolEx(ref m_ceguidDB, "\\pim.vol", IntPtr.Zero, OPEN_EXISTING);
int m_ceoidCallLog = 0;
IntPtr m_hDBCallLog = CeOpenDatabaseEx2(ref m_ceguidDB, ref m_ceoidCallLog, "clog.db", IntPtr.Zero, CEDB_AUTOINCREMENT, IntPtr.Zero);
int ceOIDTemp = CeSeekDatabase(m_hDBCallLog, CEDB_SEEK_BEGINNING, 0, IntPtr.Zero);

To identify the fields of each records to read, we need to define an array of CEPROPID. Luckily, CEPROPID is just an integer which has to be calculated by providing the column index and the data type to the MAKELONG macro. In C#, however, we can simplify this by pre-calculating the values:

//array containing the property to be retrieved
const int colStartTime = 131136;
const int colEndTime = 196672;
const int colCallNumber = 393247;
const int colCallName = 458783;
const int colCallFlags = 262147;
int[] propsToRead = new int[5] { colStartTime, colEndTime, colCallNumber, colCallName, colCallFlags };
We can proceed to read the record as follows:
int wProps = 5; //length of property array
int dwBuf = 0; //size of read buffer is returned here
byte* retVal; //pointer to returned buffer
//perform actual reading of the record
fixed (int* pArray = propsToRead)
{
int readRecordID = CeReadRecordPropsEx(m_hDBCallLog, CEDB_ALLOWREALLOC, ref wProps, new IntPtr((void*)pArray), &retVal, ref dwBuf, IntPtr.Zero);
}

The record is read properly (the return value readRecordID is valid and retVal seemingly contains data) but retrieving the read data is difficult since the returned value (retVal) is an array of unions which cannot be declared via managed code. To simulate union in .NET, we have tried to explicitly declare the start address of each member in the structure to be the same, but this result in a TypeLoadException

unsafe struct CEBLOB {
int dwCount;
byte* lpb;
};
[StructLayout(LayoutKind.Explicit)]
struct CEVALUNION
{
[FieldOffset(0)]
Int16 iVal;
[FieldOffset(0)]
Int16 uiVal;
[FieldOffset(0)]
long lVal;
[FieldOffset(0)]
Int32 ulVal;
[FieldOffset(0)]
FILETIME filetime;
[FieldOffset(0)]
string lpwstr;
[FieldOffset(0)]
CEBLOB blob;
[FieldOffset(0)]
bool boolVal;
[FieldOffset(0)]
Int64 dblVal;
}

There is no solution for this as of now.


References:

  1. http://www.codeproject.com/KB/mobile/wm_callhistory.aspx?display=Print - Retrieving and Storing Call History
  2. http://blogs.msdn.com/windowsmobile/archive/2005/05/10/416117.aspx - Porting applications from CEDB to EDB
  3. http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1838730&SiteID=1 - Structure of clog.db table
  4. http://forums.microsoft.com/msdn/ShowPost.aspx?PostID=192592&SiteID=1 - EDB samples for WM5

20 comments:

  1. I tried C# code, it can get "true" value when pass to open Volume(pim.vol), but it cannot open table( clog.db).

    I am trying it on Windows Mobile Emulator 6.

    Any suggestion.

    Thanks

    ReplyDelete
  2. Hi

    Thanks for your comment.

    It is recommended that you use the C++ version, and if needed, call it from C# via P/Invoke. The C# code posted here is incomplete and require extensive modifications to be usable. I can send you a sample Visual Studio Solution demonstrating how to to put the C++ code in a library and call it from C#.

    The table "clog.db" may not be accessible if the phone is off or if the history is empty. To try it on the emulator, you must first connect the emulator to the Cellular Emulator, make a few test calls to create some entries in the call history. Only then should you try to read the history.

    ReplyDelete
  3. Thanks for your reply.
    Can u please provide me working Project of Read write call log in C#?

    My email id: memon42@yahoo.com

    ReplyDelete
  4. If you cannot provide me C# conversion then please provide me sample as you said above "I can send you a sample Visual Studio Solution demonstrating how to to put the C++ code in a library and call it from C#."

    Thanks

    ReplyDelete
  5. Hi

    Sorry for late reply. Give me some time, will send you a sample VS solution as described to your email in the next few days.

    ReplyDelete
  6. Hi,

    I solved it using C#. Thanks for all help. Can u please tell me how can I delete SMS?

    ReplyDelete
  7. Glad you get it working.

    If you're talking about deleting SMS on Windows Mobile, you can use Messaging API (MAPI). This is mostly done via C++ but there are some attempts to do it on C# (.NET).

    Refer to this http://www.codeproject.com/KB/windows/PocketPCandSmartphone.aspx for more info.

    ReplyDelete
  8. Thanks MD; "http://www.codeproject.com/KB/windows/PocketPCandSmartphone.aspx " reference helped me. This is excellent place.

    Thanks indeed.

    Is it possible to hide incoming or outgoing call notifications?

    Regards,

    ReplyDelete
  9. For incoming call, you may want to have a look at:

    http://social.msdn.microsoft.com/Forums/en-US/vssmartdevicesvbcs/thread/87dec657-b051-4dcb-bd52-2904fbee8d4a.

    For outgoing call, try to capture the events SystemState.PhoneXXXXXX

    http://msdn.microsoft.com/en-us/library/microsoft.windowsmobile.status.systemstate_properties.aspx

    ReplyDelete
  10. I can capture Outgoing call by SystemState(SystemProperty.PhoneCallCalling)

    but how can I hide screen that say "Dialing..." and after connecting "Connected".

    I tried it with :
    FindWindowCE("Dialog", "Dialing...")

    But it did not work.

    Please help me to hide 2 screens "Dialing..." and "Connected"

    Warm Regards,

    ReplyDelete
  11. You can try with FindWindow and DestroyWindow to close Window. To decide which window to find/close, use Remote Spy, available at Start>Programs>Microsoft Visual Studio>Visual Studio Remote Tools>Remote Spy.

    Another way (not recommended) is to kill the dialer process, CPROG.EXE.

    If you simply want to terminate the outgoing call, upon receving SystemProperty.PhoneCallCalling, try to simulate the press of the red key by passing System.Windows.Forms.Keys.F4 as bVk parameter of keybd_event funtion:

    [DllImport("coredll.dll")]
    static extern void keybd_event(byte bVk, byte bScan, uint dwFlags,
    UIntPtr dwExtraInfo);

    ReplyDelete
  12. Hi,

    I am back again. I have learned many things by your support.

    I have found a bug in application, I could not understand reason:

    Suppose I have 3 numbers in log history the numbers(444 444 444, 666 6666, 555 555 555), when I executes the c++ code attached here, it deleted the middle, instead of 444 444 444(the most recent). How can I fix it? If all three numbers are of equal length then it works fine.

    Thanks

    ReplyDelete
  13. Hi

    Glad you have learned something new from my blog :)

    I cannot see your source code. Maybe blogspot comments do not allow attachments. Can you send your source code to "m d a n h 2 0 0 2 AT g m a i l . com" (without the quotes and the space). I will have a look and help your further.

    Thanks

    ReplyDelete
  14. I have sent you email with code.

    Thank you

    ReplyDelete
  15. Your advise has worked. This is really nice blog. Keep it up.

    Thank you

    ReplyDelete
  16. Glad you got it working and thanks for all your comments :)

    ReplyDelete
  17. Hi,

    I need some more help regarding Call History, how can I get call start and call end time in c#, I am not getting c++ version for this. I know, it is implemented in c++, please help me to get start and end time of call.

    How can I convert this piece of code and how to get pPropVals:

    for (int i = 0; i < wProps; i++)
    {
    //the lower word of 'propid' determines the datatype of the property (found in windbase.h)
    //the higher word of 'propid' determine what the property is for
    printf("Property %d propid=%d Value= ", i, pPropVals[i].propid);
    if (pPropVals[i].wFlags == CEDB_PROPNOTFOUND)
    {
    //this flag in ON when the property is not available
    printf("Not available.");
    }
    else
    {
    //print the property depending on the datatype
    switch(LOWORD(pPropVals[i].propid))
    {
    case CEVT_LPWSTR:
    OutputDebugString(pPropVals[i].val.lpwstr);
    break;
    case CEVT_BOOL:
    printf("%d", pPropVals[i].val.boolVal);
    break;
    case CEVT_I2:
    printf("%d", pPropVals[i].val.iVal);
    break;
    case CEVT_I4:
    printf("%d", pPropVals[i].val.lVal);
    break;
    case CEVT_R8:
    printf("%d", pPropVals[i].val.dblVal);
    break;
    case CEVT_UI2:
    printf("%d", pPropVals[i].val.uiVal);
    break;
    case CEVT_UI4:
    printf("%d", pPropVals[i].val.ulVal);
    break;
    case CEVT_FILETIME:
    ft = pPropVals[i].val.filetime;
    FileTimeToLocalFileTime(&ft, &ftLocal); //take into account device timezone settings
    FileTimeToSystemTime(&ftLocal, &st); //convert to system time for printing
    printf("%d:%d:%d %d/%d/%d", st.wHour, st.wMinute, st.wSecond, st.wDay, st.wMonth, st.wYear);




    break;
    default:
    printf("Non-printable.");
    break;
    }
    }
    printf("\n");
    }

    Thank you

    ReplyDelete
  18. Hi

    As mentioned in the post, it's not easy to convert the code you wanted to C#, since CeReadRecordPropsEx stores the return value in a C++ union, and I found no way to declare a union properly in C# via P/Invoke.

    Your only choice is to put all the C++ code in a native library, then P/Invoking that library from C#.

    ReplyDelete
  19. Hi,

    I have requested help via email, I have sent you code via email, please check and help me to fix it.

    I am really stuck with issue as I am newbie to vc++. Looking forward for your help.

    Thanks and Regards,

    ReplyDelete
  20. I want to use pin less calling with registered numbers but i also want to control my bill by viewing and tracking call history online from all registered numbers .

    ReplyDelete

Note: Only a member of this blog may post a comment.