刚搞了个黑莓玩, 在写短信时发现缺少了一个在用wm时常用的功能: 往短信中插入联系人. 这个有啥用呢, 想一想当有朋友问题你要另外一个人的号码时, 你就会知道它的用处了, 简单的通过一个菜单就可以插入联系人的手机号, 邮箱…… 而在黑莓里是没有这个功能的, 只能先切换到桌面然后打开联系人界面然后再找到你要的联系人, 然后再把电话复制出来, 再然后就是切换到短信程序并粘贴电话号码, 这显然是一个比较烦琐的步骤. 再假如你想要同时插入邮件/手机号, 则还要来回复制两遍. 所以我们就会看到一些常用的扩展来辅助插入手机号. 于是闲来无事自己研究了一下具体实现 (已经看到了一些具体的实现, 只是好奇, 所以研究了一下)
让我们分析一下这个问题, 要实现这个功能, 应该可以把功能划分为以下三个步骤:
- 往编辑短信的菜单中添加一个菜单项
- 当用户点击这个菜单的时候显示一个选择联系人的界面
- 当用户选择联系人之后把联系人的电话插入到短信中去.
下面我们分别看这三个步骤的实现, 步骤一很简单, BB提供了相关的api, 在它的文档中就可以找到, 具体请看这里. 首先我们需要实现一个派生自MenuItem类,
class InsertContactMenuItem extends ApplicationMenuItem {
InsertContactMenuItem() {
super(2000000); // This sets the item just before the "Add To:" menuitem }
public String toString() { return "Insert Contact";
}
public Object run(Object context) { ....
}
上面就是一个简单的程序菜单项, 它在初始化的时候通过调用父类的构造函数来调整它在菜单项中的顺序(这个没有试过, 具体不知道工作不工作…), toString方法返回的字符串决定了它显示在菜单中的文字, 至于run方法则会在点击菜单项时会调用到, 它接受一个context参数, 系统会传入一个上下文相关的对像.
有了Menu类之后就可以把它注册到短信编辑界面里去了:
private static void registerMenuItem() {
System.out.println("*** Registering Insert contact menuitem");
InsertContactMenuItem menuItem = new InsertContactMenuItem();
ApplicationMenuItemRepository amir = ApplicationMenuItemRepository.getInstance();
amir.addMenuItem(ApplicationMenuItemRepository.MENUITEM_SMS_EDIT, menuItem);
}
上面的逻辑很简单, 首先取得系统ApplicationMenuItemRepository的实例, 并向注册InsertContactMenuItem到SMS_EDIT中去, 好了, 到了这里, 第一步基本完成了. 完成这一步, 你就可以在模拟器里看到相应的菜单项了, 如下图:

下面开始第二步, 这里需要在用户点击菜单之后弹出一个选择联系人的界面, 这个可以通过BlackBerryContactList.choose()方法实现:
PIM pim = PIM.getInstance();
BlackBerryContactList list = (BlackBerryContactList) pim.openPIMList(PIM.CONTACT_LIST, PIM.READ_ONLY);
PIMItem pimItem = list.choose ();
String phoneNum = pimItem.getString (Contact.TEL, 0);
这里首取得系统PIM的实例, 然后联系人列表并调用它的choose ()方法, 运行的结果应该如下图所示, 出现一个选择联系人的窗口:
到了这里, 最后一步就是要把联系人信息插入到短信输入窗口里了, 在这里我走了一段弯路:
在前面提到在系统调用run (Object context)方法时会传入一个上下文相关的对象, 在看4.5.0os的文档里提到这里会传入一个TextMessage对象, 具体参见这里, 当时我是想尝试直接通过修改这个对象来往消息里插入文本, 不过可惜的是行不能: 因为在这里传入的TextMessage对象是只读的, 尝试调用它的set方法会引发一个异常. 而且在老版本的系统里根本就没有传入一个对象(8800 4.2.0模拟器里显示传入的对象为空). 所以在经历了若干次失败后只好转求它法.
在阅读相关的代码的时候, 终于发现了一个UiApplication类, 通过它可以得到当前在最前面的屏幕, 并且我们可以遍历这个屏幕上的所有元素, 貌似有点希望! 让我们尝试遍历一当前屏幕并打印出这些元素, 看看它们有些什么!
Screen screen = UiApplication.getUiApplication().getActiveScreen();
for (int i = 0; i < screen.getFieldCount(); i++)
{
Field field = screen.getField(i);
printFields (field);
}
void printFields (Field field)
{
try
{
System.out.println("*** Field class: " + field.getClass().getName());
if (field instanceof TextField) {
TextField textField = (TextField) field;
System.out.println("*** Found a text field");
System.out.println("*** text field label: "
+ textField.getLabel());
System.out.println("*** Found text field");
System.out.println ("** value: " + textField.getText ());
}
else if (field instanceof Manager)
{
Manager mgrField = ( Manager) field;
System.out.println ("** Subfields..");
for (int i = 0; i < mgrField.getFieldCount(); i++) {
Field subFields = mgrField.getField(i);
printFields (subFields);
}
}
}
catch (Exception e)
{
System.out.println ("printFields excetion:" + e.getMessage ());
}
}
对于这段代码, 前几行是得到当前活动的屏幕, 然后调用函数printFields函数, 在这个函数里我们基本是把一个field相关信息打印了出来: 它的标签的文字, 它的文字. 由于在screen里可以嵌套包含一些屏幕元素的管理对象, 所以我们需要一个递归来打印. 下面是得到的结果, 请注意使用蓝颜色标记的文字,
BB 8800 4.2.1.74的Debug输出
JVM: bklt @8044: timer
JVM: bklt @8044: idle 0
JVM: bklt @8044: setTimer 22
*** Field class: net.rim.device.api.ui.container.VerticalFieldManager
** Subfields..
*** Field class: net.rim.device.apps.internal.phone.model.ReadOnlyPhoneNumberField
** Subfields..
*** Field class: net.rim.device.apps.internal.phone.model.SmartPhoneNumberLabelField
*** Field class: net.rim.device.api.ui.component.SeparatorField
*** Field class: net.rim.device.apps.internal.sms.ui.SMSEditField
*** Found a text field
*** text field label: null
*** Found Using field
** value: Afdsf
BB 9500 的Debug输出:
FRIDG: could not find background_popup__Landscape.png
*** Field class: net.rim.device.api.ui.container.VerticalFieldManager
** Subfields..
*** Field class: net.rim.device.apps.api.ui.LeftRightFieldManager
** Subfields..
*** Field class: net.rim.device.api.ui.container.HorizontalFieldManager
** Subfields..
*** Field class: net.rim.device.api.ui.component.LabelField
*** Field class: net.rim.device.apps.internal.sms.ui.MessageComposeComboField
** Subfields..
*** Field class: net.rim.device.apps.api.addressbook.AddressBookComboField$EmailComposeEditable
*** Found a text field
*** text field label: To:
*** Found text field
** value: 12313
*** Field class: net.rim.device.apps.api.ui.LeftRightFieldManager
** Subfields..
*** Field class: net.rim.device.api.ui.container.HorizontalFieldManager
** Subfields..
*** Field class: net.rim.device.api.ui.component.LabelField
*** Field class: net.rim.device.apps.internal.sms.ui.MessageComposeComboField
** Subfields..
*** Field class: net.rim.device.apps.api.addressbook.AddressBookComboField$EmailComposeEditable
*** Found a text field
*** text field label: To:
*** Found text field
** value:
*** Field class: net.rim.device.apps.internal.smscompose.message.SMSEditorScreen$BodyVerticalFieldManager
** Subfields..
*** Field class: net.rim.device.apps.internal.smscompose.ui.SMSEditField
*** Found a text field
*** text field label: null
*** Found text field
** value: Wet
我们可以看到实际在这两个系统里的短信编辑输入框实际都是一个net.rim.device.apps.internal.smscompose.ui.SMSEditField 类, 看上去似乎我们可以通过判断一个Field对象是不是一个此类的实例来找到短信的实际输入框了, 十分遗憾的是请注意前面的那一连串类名里的一个.internal.: 对不起, 这是一个内部类, 外界开发人员是不能使用这个类的.不过我们还是可以利用这一点的, 在这里直接判断它的类名里是不是包含SMSEditField就可以达到预定效果.
Screen screen = UiApplication.getUiApplication().getActiveScreen();
for (int i = 0; i < screen.getFieldCount(); i++)
{
Field field = screen.getField(i);
printFields (field, phoneNum);
}
void printFields (Field field, String str)
{
try
{
System.out.println("*** Field class: "
+ field.getClass().getName());
if (field instanceof TextField) {
TextField textField = (TextField) field;
System.out.println("*** Found a text field");
System.out.println("*** text field label: "
+ textField.getLabel());
System.out.println("*** Found text field");
System.out.println ("** value: " + textField.getText ());
if (textField.getClass ().getName ().endsWith(".SMSEditField")
{
System.out.println ("insertting text to...");
textField.insert (str);
}
}
else if (field instanceof Manager)
{
Manager mgrField = ( Manager) field;
System.out.println ("** Subfields..");
for (int i = 0; i < mgrField.getFieldCount(); i++) {
Field subFields = mgrField.getField(i);
printFields (subFields, str);
}
}
}
catch (Exception e)
{
System.out.println ("printFields excetion:" + e.getMessage ());
}
}
}
修改完代码后, 编译, 运行, 启动程序, 然后打开短信输入窗口, 按照前面的步骤选择联系人, 确认! 电话被成功的插入到短信编辑窗口里了!:)

很好, 这里我们基本大功告成了! 不过其实还有很事情要做: 怎么把程序弄成开机启动的, 然后应该在选择联系人之后我们应该弹出一个对话框让用户选择需要插入的项(移动电话, 家庭电话, email 等). 不过做为一个自己瞎折腾型的项目, 到这里咱们就告一段落了:)
最后再告诉你一个不幸的消息, 即便你有了这个程序你也不能把它安装到你的手机里的, 这是因为黑莓开发的api中有一些是需要官方的验证的, 而这个验证在当前是需要钱滴(20刀). 不过即使没有申请官方的验证也是可以在模拟器里支持你的程序的, 这就是我写这篇文章的环境:)
具体的代码在这里:InsertContact.java