65.9K
CodeProject 正在变化。 阅读更多。
Home

检索和存储通话记录

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2007 年 12 月 19 日

CPOL

5分钟阅读

viewsIcon

107364

downloadIcon

1365

本文介绍如何为原生 Phone API 创建一个包装类,然后使用它来检索和存储呼叫历史记录。

Screenshot

Screenshot

引言

下面的代码展示了如何用 C# 包装原生 Windows Mobile 6 API,以便可以将其与 .NET Compact Framework 和目标应用程序一起使用。具体来说,它展示了一个包装必要结构和函数的示例,以能够检索呼叫历史记录。然后,呼叫历史记录将被存储在数据库中以供查看。

我再次决定使用 SQL Anywhere 10 数据库。这主要是因为它占用的空间非常小,并且拥有集成的 Web 服务器。基本上,下面的示例允许您通过 Internet Explorer 查看呼叫历史记录。您可以在以下网址阅读有关其产品的信息:http://www.sybase.com/products/databasemanagement/sqlanywhere

背景

虽然 Windows Mobile 6 支持 .NET Compact Framework,但许多特定于手机的功能只能通过原生 Win32 API 访问。请参阅 Microsoft 的参考资料:http://msdn2.microsoft.com/en-us/library/bb416430.aspx。通过使用 C# 和 .NET 的互操作性可以弥合这一差距。

一旦能够访问呼叫历史记录,就可以对其进行一些操作了。我一直渴望“既要又要”。因此,有什么比将数据公开为可以任意使用的数据的 Web 服务更好的方式呢?

原生 C++ Win32 Phone API

首先,您需要查看该库的规范。我只对访问呼叫历史记录感兴趣,所以我将只讨论那些部分。

Phone API 函数

// 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 结构

// 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 枚举

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

使用 C# .NET 2.0 包装原生 API

包装函数

现在,您需要用 C# 为函数创建原型,并指示编译器从 Native DLL 中获取函数。这是所有任务中最容易的,但这里有一些技巧需要掌握。您会注意到下面我们没有使用指针,而是使用了函数中的 outref 等关键字。

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

HRESULT 类型定义与 IntPtr 兼容,尽管 HRESULT 是无符号的,而 IntPtr 是有符号的。

请注意,当我们期望函数填写指针的值时,使用了 out;当我们期望函数填写结构或变量时,使用了 ref

包装结构

现在,我们需要创建兼容的类,这些类可以被 Native DLL 填充,但可以被我们的 C# 应用程序使用。

这有两种方式完成;首先创建一个 C++ 可以使用的兼容类,然后创建一个更简洁的 C# 类。

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

您会注意到使用了 [StructLayout()]。这是因为我们需要结构 **完全** 匹配 Native DLL 的期望(换句话说,它需要位兼容)。由于 .NET **Compact** Framework 不支持所有 StructLayout,因此我们必须明确告诉编译器每个数据块应该放在哪里。

接下来,我们可以创建利用 C# 类型和类的 C# 类,以存储更可用的数据形式。我省略了重写所有访问器,但显然,每个成员都需要一个访问器。

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 ... */
  }
}
}

访问器的 set 方法和构造函数都是 internal 的,因为除了我们之外,没有人应该能够实际创建或编辑通话记录条目!

包装枚举

所以,也许我说函数是最容易包装的有点撒谎。枚举非常简单,您只需要做两次:一次是为了保持命名和值顺序与 Native DLL 一致,另一次是为了创建“更简洁”的枚举。这有点不必要的过度,但将原生代码和托管代码真正分开是一个好习惯。

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
    }
  }
}
}

在我们“简洁”的枚举中,我们实际上不需要 CALLLOGSEEK 枚举,因为它没有用。

using System;
using System.Runtime.InteropServices;

namespace WindowsMobile6
{
namespace Phone
{
  public enum CallerIDType
  {
    Unavailable,
    Blocked,
    Available
  }
  public enum Iom
  {
    Missed,
    Incoming,
    Outgoing
  }
}
}

包装返回代码

最后,您应该创建命名相似的返回值,以检查函数是否正常执行。您可以包装所有这些,但您只需要一个(用于检查成功,因为任何其他结果都是错误的)。

private const Int64 S_OK = 0x00000000;

只需确保使用兼容的检查

IntPtr HRESULT = SomeNativeWrappedFunction();
      
// Notice the .ToInt64()

if(HRESULT.ToInt64() != S_OK)
{
    throw new Exception("Error!");
}

整合

一旦所有接口都已包装,我们就需要对它们做一些事情(即检索呼叫历史记录)。

请注意 unsafe 关键字的使用。这是因为当我们使用指针时,我们不希望垃圾回收器移动我们的结构。

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

我已将所有这些代码放在一个 C# DLL 中,现在可以在任何 .NET 应用程序中使用。现在,我们可以使用我们的托管 Phone Library 来完成检索和存储呼叫历史记录的任务。

存储数据

您需要一个表来存储数据,一个存储过程来检索和格式化数据,然后一个 Web 服务来公开它。只需使用交互式 SQL 命令执行器运行以下脚本。

创建用户

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

创建表

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

创建存储过程

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

创建 Web 服务

CREATE SERVICE "root" TYPE 'RAW' AUTHORIZATION OFF USER 
  "webservice_user" AS call PHONE.GetCallHistory();

使用 C# .NET 2.0 类在数据库中检索和存储呼叫历史记录

到目前为止,我们所做的一切都是为了达到这一点。首先,我们需要引用我们刚刚创建的 WindowsMobile6.Phone 库。因为我使用的是 SQL Anywhere 10,所以我必须引用其 DLL 的 CE 版本。

using WindowsMobile6.Phone;
using iAnywhere.Data.SQLAnywhere;

所以现在,检索呼叫历史记录就简化为一个简单的属性调用: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();

使用 Web 服务(即查看数据)

因为 SQL Anywhere 数据库中有内置的 Web 服务,您只需要打开 Internet Explorer。

© . All rights reserved.