Windows Mobile 通话静音器






4.91/5 (11投票s)
用于在电脑上显示来电显示信息并在通话时静音电脑声音的程序。

引言
通过一点C#代码,我为我的Windows Mobile手机构建了一个解决方案,当手机响铃时,它会在我的桌面上显示来电信息,并在手机通话时静音我的电脑,或者口头播报来电者的姓名。通话结束后,电脑会自动取消静音。
此程序的背后故事
我的电脑上有很多程序会出于各种原因发出声音。有即时消息或电子邮件的通知,有计划的提醒,有媒体播放器,还有偶尔意外发出声音的网络广告。无论声音的目的是什么,我都不希望在通话时播放。如果我忘记静音电脑,然后收到了一个意外的、非常响亮的通知,这会影响到电话的另一端。当我静音电脑时,我可能会忘记取消静音,从而错过新消息和提醒的通知。为了解决这个问题,我决定让我的手机在通话时静音我的电脑,并在通话结束后取消静音。
我认为有必要展示手机状态信息的使用,使其不仅限于静音电脑,由此产生了两个附加功能。我为该系统的桌面客户端添加了功能,可以在通知气球中显示来电信息。我还添加了一个程序,该程序利用Vista的语音合成功能来口头播报来电者的姓名。
要求
运行在手机上的代码部分需要Windows Mobile Professional设备(带触摸屏的设备)。运行在桌面上的程序需要Windows Vista。此代码不适用于Windows XP。
支持文章
为了处理电脑音频系统的静音和取消静音,我使用了Ray M.关于Vista Core Audio API主音量控制的文章中的代码。如果您计划使用此代码并拥有64位系统,请阅读他文章的评论部分;其中列出了您必须对代码进行的一项更改,否则代码将无法正常工作。
查询手机状态
对于这个程序,我们必须能够查询手机的状态。这包括检测来电、知道来电者是谁、检测是否提供数据连接等等。所有这些信息都通过状态和通知代理进行跟踪。在.NET Compact Framework中,状态和通知代理可以通过SystemState
类进行访问。如果您查看SystemState
类的成员,您会发现它可用于获取数据连接是否存在的信息、有关当前和上一次通话的信息以及大量的其他信息。
SystemState
类易于使用。创建该类的实例时,开发者必须指定需要通知更改的元素。然后,每次该值发生更改时,都会引发一个事件。本文档附带一个名为PhoneStatus
的示例程序。它使用SystemState
类来检测手机是否处于活动状态,是否存在数据连接,并将显示来电者的姓名。

SystemState _statePhoneIncoming;
SystemState _StatePhoneActiveCount;
SystemState _phoneCaller;
SystemState _localData;
public MainForm()
{
InitializeComponent();
_statePhoneIncoming = new SystemState(SystemProperty.PhoneIncomingCall, true);
_statePhoneIncoming.Changed += new ChangeEventHandler(_statePhoneIncoming_Changed);
_StatePhoneActiveCount = new SystemState(SystemProperty.PhoneActiveCallCount, true);
_StatePhoneActiveCount.Changed += new ChangeEventHandler
(_StatePhoneActiveCount_Changed);
_localData = new SystemState(SystemProperty.ConnectionsCount, true);
_localData.Changed += new ChangeEventHandler(_localData_Changed);
_phoneCaller = new SystemState(SystemProperty.PhoneIncomingCallerNumber, true);
_phoneCaller.Changed += new ChangeEventHandler(_phoneCaller_Changed);
}
void _phoneCaller_Changed(object sender, ChangeEventArgs args)
{
lblLastCallerName.Text = SystemState.PhoneIncomingCallerName;
lblLastCallerNumber.Text = SystemState.PhoneIncomingCallerNumber;
}
void _localData_Changed(object sender, ChangeEventArgs args)
{
chkLocalData.Checked = ((int)args.NewValue) > 0;
}
void _StatePhoneActiveCount_Changed(object sender, ChangeEventArgs args)
{
chkCallActive.Checked = (SystemState.PhoneActiveCallCount > 0) ||
SystemState.PhoneIncomingCall;
}
void _statePhoneIncoming_Changed(object sender, ChangeEventArgs args)
{
chkCallActive.Checked = SystemState.PhoneIncomingCall ||
SystemState.PhoneActiveCallCount > 0;
}
发送静音和取消静音消息
除了自动静音电脑外,用户还可以使用手机来静音或取消静音电脑。发送静音和取消静音消息的界面只需一张图片即可说明。

将状态发送到桌面
我使用的Windows Mobile Professional手机有两个适配器可以与我的电脑通信;一个是WiFi适配器,另一个是蓝牙适配器。该应用程序通过UDP进行通信。因此,它可以在带有WiFi适配器的手机上运行,也可以在蓝牙个人区域网络上的设备上运行。但是我的电脑没有蓝牙功能,所以我只使用手机的WiFi适配器测试过这个程序。最初,我只需要程序发送四条消息。为了表示手机的活动,有“active”和“inactive”消息。程序还允许发送“mute”和“unmute”消息来更改计算机音频硬件的状态。后来我决定添加“CallerID
”消息。
选择接收机器
有三种方法可以通过UDP将消息发送到计算机组。可以将消息广播到网络节点上的所有计算机,可以将其发送到组播组(在这种情况下,对某些消息感兴趣的计算机可以注册以接收它们),或者可以将消息单独发送到每台计算机。UDP广播已被弃用;它在IPv4上仍然有效,但它不是IPv6标准的一部分。组播本应更理想,但我发现我连接的一些路由器不允许我的机器发送组播流量。与其说服路由器所有者为我的个人需求重新配置路由器,不如直接将信息发送到应该接收它的机器。下面的UI允许用户输入应该更新手机状态信息的设备的名称或IP地址,并指定所有设备将监听的端口。

消息安全性
在实现此程序时,我花了很多时间考虑安全性。最初,这个程序只需要在我手机激活时静音和取消静音我的电脑。我对所使用的网络的安全性和网络用户的良性意图很有信心。但在开发此程序的过程中,职业责任促使我考虑其他因素。当来电显示信息被添加到此程序传输的消息中时,安全性变得更加重要。通过加密消息,我得以解决消息安全问题。我使用的是.NET Compact Framework实现的Rijndael加密算法(也称为AES)。程序启动时,它会在其目录下查找已保存的加密密钥。如果找不到,它将创建一个新的加密密钥。该密钥必须从设备复制并导入到手机将共享状态的所有计算机中。该系统的桌面部分在文件菜单中有一个导入加密密钥的选项。
void SendMessage(byte[] message)
{
for (int i = 0; i < _clientList.Count; ++i)
{
//attempt to send each message to the clients individually
try
{
UdpClient client = _clientList[i];
byte[] encryptedMessage = Encrypt(message);
client.Send(encryptedMessage, encryptedMessage.Length);
}
catch (Exception )
{
//If a message could not be sent simply do nothing.
}
}
}
byte[] Encrypt(byte[] message)
{
MemoryStream ms = new MemoryStream();
CryptoStream encStream = new CryptoStream(ms, _transform, CryptoStreamMode.Write);
encStream.Write(message, 0, message.Length);
encStream.FlushFinalBlock();
encStream.Close();
return ms.ToArray();
}
接收消息
此解决方案的桌面部分将等待要处理的手机活动消息。使用UdpClient
类接收消息将涉及调用其Read
方法。该类的Read
方法是一个阻塞方法;调用它的线程将暂停,直到有消息可读。在单线程应用程序中,在UI线程上调用UdpClient
会使程序在收到消息之前看起来处于冻结状态。为防止这种情况发生,必须在单独的线程上执行对UdpClient.Read
的调用。
监听线程的管理以及接收消息的功能被打包在一个实现了IMessageListener
接口的类中。该接口列出了两个方法(Activate
和Deactivate
)用于启动和停止监听消息,一个属性用于分配ICryptoTransform
对象以解密消息,以及事件处理程序以提供传入消息和实现类监听状态变化的通知。该接口定义如下:
public interface IMessageListener: IDisposable
{
ICryptoTransform CryptoProvider { get; set; }
void Activate();
void Deactivate();
event EventHandler MessageReceived ;
event EventHandler ListenerStateChanged ;
}
Activate
和Deactivate
的实现是在IMessageListener
接口的实现中可能变化的部分。MessageListener abstract
类包含了监听器实现中不太可能变化的部分。监听UDP端口的细节在UdpMessageListener
类中定义。如果您想实现另一个监听器,您需要:
- 创建一个继承自
MessageListener
的类 - 定义
Activate
和Deactivate
方法 - 适当地调用
MessageReceived
和ListenerStateChanged
事件

一旦正确实现,该类将通过MessageReceived
事件处理程序的MessageReceivedEventArgs
参数传递未加密的消息。
处理消息
消息的处理由一个switch
语句处理。该语句通常会更改计算机声音设备的静音状态,并更新状态文本以指示手机的状态。如果消息是CallerID
消息,则设备将通过通知气球显示CallerID
信息。
switch (e.MessageType)
{
case MessageType.Active:
if (!_senderActive)
previouslyMuted = device.AudioEndpointVolume.Mute;
_senderActive = true;
device.AudioEndpointVolume.Mute = true;
UpdateStatus("Phone Active");
break;
case MessageType.Inactive:
_senderActive = false;
device.AudioEndpointVolume.Mute = previouslyMuted;
UpdateStatus("Phone Inactive");
break;
case MessageType.Mute:
device.AudioEndpointVolume.Mute = previouslyMuted = true;
UpdateStatus("Mute request received");
break;
case MessageType.Unmute:
previouslyMuted = false;
device.AudioEndpointVolume.Mute = false;
UpdateStatus("Unmute request received");
break;
case MessageType.CallerID:
callerIdNotification.Visible = true;
callerIdNotification.ShowBalloonTip(9000,"Incoming Call",String.Format
("Call received from {0} ({1})", e.Name, e.Number),ToolTipIcon.Info);
break;
default:
break;
}
来电播报
“来电播报”程序将口头播报来电者,并维护一个已接收电话的列表。此程序背后的实际代码非常简单。该项目使用了相同的MessageListener
类。为了口头播报来电者的姓名,该程序使用了.NET 3.0 Framework的SpeechSynthesizer
类。该类依赖于Windows Vista附带的原生功能,但不是Windows XP的一部分。要测试此程序,您必须使用运行Vista的计算机。该类有两种渲染口头播报名称的方法:Speak
和SpeakAsync
。顾名思义,Speak
是一个阻塞调用,而SpeakAsync
允许您的代码在语音播放时继续执行。

将调用封送到UI线程
在代码中,您偶尔会看到通过Control.Invoke
调用自身的方法。出现这种模式是因为除了主线程之外,任何线程都不能修改UI元素。在其他线程上发生的调用将无法更改textbox
中显示的文本、checkbox
的选中状态或任何其他视觉元素。控件具有Control.InvokeRequired
属性,当由辅助线程调用时返回true
,当在UI线程上调用时返回false
。我使用Control.InvokeRequired
来确定是否需要将调用封送到主线程。如果需要封送,则使用Control.Invoke
来确保调用在正确的线程上发生。以下是使用此模式更新状态栏文本的UpdateStatus
方法的示例:
void UpdateStatus(String statusMsg)
{
if (this.InvokeRequired)
{
this.Invoke(_updateText, new object[] { statusMsg });
}
else
{
txtFeedback.Text = statusMsg;
}
}
未来的增强
对于此代码的初始版本,我的目标很简单;我只是希望代码能够正常工作。现在我已经实现了这个目标,有一些可用性增强功能可以添加,例如在手机连接到计算机时自动传输加密密钥。几周后,我应该会收到一个蓝牙适配器和一个Windows Mobile Standard手机,可能会修改程序使其在没有触摸屏的手机上以及通过蓝牙(除了UDP)工作。
历史
- 2008年12月4日 - 首次发布