/* * PhoneApiDemo.java * * A simple program that demonstrates the Phone API. Persistently stores the * "talk time" for each phone number contacted while the application is running. * * Copyright � 1998-2011 Research In Motion Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Note: For the sake of simplicity, this sample application may not leverage * resource bundles and resource strings. However, it is STRONGLY recommended * that application developers make use of the localization features available * within the BlackBerry development platform to ensure a seamless application * experience across a variety of languages and geographies. For more information * on localizing your application, please refer to the BlackBerry Java Development * Environment Development Guide associated with this release. */ package com.rim.samples.device.phone.phoneapidemo; import java.util.Date; import java.util.Hashtable; import java.util.Vector; import net.rim.blackberry.api.phone.AbstractPhoneListener; import net.rim.blackberry.api.phone.Phone; import net.rim.blackberry.api.phone.PhoneCall; import net.rim.device.api.command.Command; import net.rim.device.api.command.CommandHandler; import net.rim.device.api.command.ReadOnlyCommandMetadata; import net.rim.device.api.system.Characters; import net.rim.device.api.system.ControlledAccessException; import net.rim.device.api.system.Display; import net.rim.device.api.system.KeyListener; import net.rim.device.api.system.PersistentObject; import net.rim.device.api.system.PersistentStore; import net.rim.device.api.ui.Color; import net.rim.device.api.ui.Field; import net.rim.device.api.ui.Manager; import net.rim.device.api.ui.MenuItem; import net.rim.device.api.ui.Screen; import net.rim.device.api.ui.UiApplication; import net.rim.device.api.ui.XYRect; import net.rim.device.api.ui.component.BasicEditField; import net.rim.device.api.ui.component.Dialog; import net.rim.device.api.ui.component.LabelField; import net.rim.device.api.ui.component.Menu; import net.rim.device.api.ui.component.table.DataTemplate; import net.rim.device.api.ui.component.table.TableController; import net.rim.device.api.ui.component.table.TableModelAdapter; import net.rim.device.api.ui.component.table.TableModelChangeEvent; import net.rim.device.api.ui.component.table.TableView; import net.rim.device.api.ui.component.table.TemplateColumnProperties; import net.rim.device.api.ui.component.table.TemplateRowProperties; import net.rim.device.api.ui.container.MainScreen; import net.rim.device.api.ui.decor.BackgroundFactory; import net.rim.device.api.util.Persistable; import net.rim.device.api.util.StringProvider; /** * The main class for the Phone API demo app. */ public final class PhoneApiDemo extends UiApplication { // Members // ------------------------------------------------------------------------------------- private final PhoneApiDemoMainScreen _mainScreen; // Statics // ------------------------------------------------------------------------------------- private static PersistentObject _persist; private static Vector _phoneNumberList; private static PhoneNumberTableModelAdapter _model; // Make sure a database exists for this application static { _persist = PersistentStore.getPersistentObject(0x15835f89fc421f8cL); // com.rim.samples.device.phone.phoneapidemo synchronized (_persist) { _phoneNumberList = (Vector) _persist.getContents(); _model = new PhoneNumberTableModelAdapter(); if (_phoneNumberList == null) { _phoneNumberList = new Vector(); _persist.setContents(_phoneNumberList); _persist.commit(); } } } /** * PhoneApiDemo Constructor. Creates the main screen for the app and pushes * it onto the display stack. */ public PhoneApiDemo() { _mainScreen = new PhoneApiDemoMainScreen(); /* parent. */pushScreen(_mainScreen); } /** * Commits the phone number records to the persistent store */ private static void savePhoneNumberRecords() { synchronized (_persist) { _persist.commit(); } } /** * Entry point for the application. On autostartup, it adds a new phone * listener for timing phone calls. On ribbon startup, a gui is presented * that allows users to view phone number "talk time" records. * * @param args * Command-line arguments (used for differentiating between * autostartup and ribbon startup). */ public static void main(final String[] args) { if (args.length == 1 && args[0].equals("autostartup")) { // Create and register the object that will listen for Phone events. // Check for // ControlledAccessException as per page 69 of the BlackBerry // Application // Developer Guide, Volume 2 (Version 4.0). try { Phone.addPhoneListener(new ConcretePhoneListener()); } catch (final ControlledAccessException e) { UiApplication.getUiApplication().invokeLater(new Runnable() { public void run() { Dialog.alert("Access to Phone API restricted by system administrator: " + e.toString()); } }); System.exit(1); } } else { // Create a new instance of the application and make the currently // running thread the application's event dispatch thread. new PhoneApiDemo().enterEventDispatcher(); } } // Private inner classes // ----------------------------------------------------------------------- /** * Adapter class for displaying phone number information in table format */ private static class PhoneNumberTableModelAdapter extends TableModelAdapter { /** * @see net.rim.device.api.ui.component.table.TableModelAdapter#getNumberOfRows() */ public int getNumberOfRows() { return _phoneNumberList.size(); } /** * @see net.rim.device.api.ui.component.table.TableModelAdapter#getNumberOfColumns() */ public int getNumberOfColumns() { return 1; } /** * @see net.rim.device.api.ui.component.table.TableModelAdapter#doGetRow(int * ) */ protected Object doGetRow(final int index) { return _phoneNumberList.elementAt(index); } /** * @see net.rim.device.api.ui.component.table.TableModelAdapter#doRemoveRowAt(int * ) */ protected boolean doRemoveRowAt(final int index) { return _phoneNumberList.removeElement(doGetRow(index)); } /** * @see net.rim.device.api.ui.component.table.TableModelAdapter#doAddRow(Object * ) */ protected boolean doAddRow(final Object object) { _phoneNumberList.addElement(object); return true; } /** * Force the table to refresh the listed elements */ public void refresh() { notifyListeners(new TableModelChangeEvent( TableModelChangeEvent.COLUMN_UPDATED, this, -1, 0)); } }; /** * The main screen for the Phone API application. It displays a list of * phone numbers that have been contacted, and allows the user to view the * record for each one. */ private final class PhoneApiDemoMainScreen extends MainScreen { // Members -------------------------------------------------- private MenuItem _deleteAllItem; private TableView _view; /** * PhoneApiDemoMainScreen constructor. Creates the fields and menu items * used on this screen. */ private PhoneApiDemoMainScreen() { super(Manager.NO_VERTICAL_SCROLL); setTitle("Phone API Demo"); _view = new TableView(_model); final TableController controller = new TableController(_model, _view); _view.setController(controller); _view.setDataTemplateFocus(BackgroundFactory .createLinearGradientBackground(Color.LIGHTBLUE, Color.LIGHTBLUE, Color.BLUE, Color.BLUE)); final DataTemplate dataTemplate = new DataTemplate(_view, 1, 1) { public Field[] getDataFields(final int modelRowIndex) { final PhoneNumberRecord record = (PhoneNumberRecord) _model.getRow(modelRowIndex); final String text = (String) record .getField(PhoneNumberRecord.PHONE_NUMBER); final Field[] fields = { new LabelField(text, Field.NON_FOCUSABLE) }; return fields; } }; dataTemplate.createRegion(new XYRect(0, 0, 1, 1)); dataTemplate.setColumnProperties(0, new TemplateColumnProperties( Display.getWidth())); dataTemplate.setRowProperties(0, new TemplateRowProperties(32)); _view.setDataTemplate(dataTemplate); dataTemplate.useFixedHeight(true); add(_view); // Menu item that deletes all the phone number records. _deleteAllItem = new MenuItem(new StringProvider("Delete All"), 0x230010, 0); _deleteAllItem.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { if (Dialog.ask(Dialog.D_DELETE) == Dialog.DELETE) { while (_model.getNumberOfRows() > 1) { _model.removeRowAt(0, true); } if (_model.getNumberOfRows() == 1) { _model.removeRowAt(0); } PhoneApiDemo.savePhoneNumberRecords(); } } })); addKeyListener(new PhoneApiDemoKeyListener(this)); } /** * @see net.rim.device.api.ui.container.MainScreen#onExposed() */ protected void onExposed() { _model.refresh(); } /** * Creates the menu for this screen * * @see net.rim.device.api.ui.container.MainScreen#makeMenu(Menu,int) */ protected void makeMenu(final Menu menu, final int instance) { super.makeMenu(menu, instance); // If there are any items in the list, add menu items to view and // delete them. if (_model.getNumberOfRows() > 0) { final int index = _view.getRowNumberWithFocus(); final PhoneNumberRecord record = (PhoneNumberRecord) _model.getRow(index); menu.add(new View(record)); menu.addSeparator(); menu.add(new Delete(record)); menu.add(_deleteAllItem); menu.addSeparator(); } } // Private inner classes representing menu items and listeners used by // this screen --------- /** * This class is a menu item allowing a user to view a phone number * record. */ private final class View extends MenuItem { // Members ---------------------------------------------- private final PhoneNumberRecord _record; /** * Constructs a menu item to view a record when invoked * * @param record * The record to view */ private View(final PhoneNumberRecord record) { super(new StringProvider("View"), 0x230020, 100); _record = record; this.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { final MainScreen screen = new MainScreen(); screen.setTitle(new LabelField("View Phone Record")); final PhoneNumberRecordDisplayer displayer = new PhoneNumberRecordDisplayer(_record); final Vector fields = displayer.getFields(); final int numFields = fields.size(); for (int i = 0; i < numFields; ++i) { screen.add((Field) fields.elementAt(i)); } screen.addKeyListener(new PhoneApiDemoKeyListener( screen)); PhoneApiDemo.this.pushScreen(screen); } })); } } /** * This class is a menu item allowing a user to delete a phone number * record */ private final class Delete extends MenuItem { /** * Constructs a menu item to delete a phone record when invoked * * @param record * The phone record to delete */ private Delete(final PhoneNumberRecord record) { super(new StringProvider("Delete"), 0x230030, 110); this.setCommand(new Command(new CommandHandler() { /** * Deletes the phone record * * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { if (Dialog.ask(Dialog.D_DELETE) == Dialog.DELETE) { _model.removeRowAt(_view.getRowNumberWithFocus()); PhoneApiDemo.savePhoneNumberRecords(); } } })); } } } /** * Persistable phone number record */ private static final class PhoneNumberRecord implements Persistable { // Members // --------------------------------------------------------------------------------- private final Vector _fields; private long _startTime; // Constants // ------------------------------------------------------------------------------- private static final int PHONE_NUMBER = 0; private static final int TALK_TIME = 1; /** * Constructs a PhoneNumberRecord from a phone number * * @param phoneNumber * The phone number of the record to be created */ private PhoneNumberRecord(final String phoneNumber) { _fields = new Vector(); _fields.addElement(phoneNumber); _fields.addElement(new Long(0)); _startTime = 0; } /** * Retrieves one of this record's fields * * @param index * The index of the field to retrieve * @return The field */ private Object getField(final int index) { return _fields.elementAt(index); } /** * Sets one of this record's fields * * @param index * The index of the field to set * @param o * The object that the field is set to */ private void setField(final int index, final Object o) { _fields.setElementAt(o, index); } /** * Determines if this record and the parameter refer to the same phone * number record. * * @param o * The object to check for equality * @return True if this record and o refer to the same phone number * record; false otherwise */ public boolean equals(final Object o) { if (o instanceof PhoneNumberRecord) { final PhoneNumberRecord record = (PhoneNumberRecord) o; final String phoneNumber1 = (String) record._fields.elementAt(PHONE_NUMBER); final String phoneNumber2 = (String) _fields.elementAt(PHONE_NUMBER); return phoneNumber1.equals(phoneNumber2); } return false; } /** * @see java.lang.Object#hashCode() */ public int hashCode() { final Object phoneNumber = _fields.elementAt(PHONE_NUMBER); return phoneNumber == null ? 0 : phoneNumber.hashCode(); } /** * Determines whether this record is currently recording "talk time" * * @return True if this record is currently recording "talk time"; false * otherwise */ private boolean isActive() { return _startTime != 0; } /** * Causes this record to start recording "talk time" */ public void start() { _startTime = new Date().getTime(); } /** * Causes this record to temporarily stop recording "talk time" */ private void putOnHold() { long talkTime = ((Long) _fields.elementAt(TALK_TIME)).longValue(); talkTime += new Date().getTime() - _startTime; _fields.setElementAt(new Long(talkTime), TALK_TIME); _startTime = 0; } /** * Causes this record to resume recording "talk time" */ private void resume() { start(); } /** * Causes this record to stop recording "talk time" */ private void end() { putOnHold(); } } /** * This class is used to display the information inside a phone number * record on the screen using fields. */ private static final class PhoneNumberRecordDisplayer { // Members ---------------------------------------------- private final BasicEditField _phoneNumber; private final BasicEditField _talkTime; /** * Constructs a collection of fields to display the phone number * record's information. * * @param phoneNumberRecord * The phone number record to display */ private PhoneNumberRecordDisplayer( final PhoneNumberRecord phoneNumberRecord) { final String phoneNumber = (String) phoneNumberRecord .getField(PhoneNumberRecord.PHONE_NUMBER); long talkTime = ((Long) phoneNumberRecord .getField(PhoneNumberRecord.TALK_TIME)).longValue(); _phoneNumber = new BasicEditField("Phone Number: ", phoneNumber); _phoneNumber.setEditable(false); // Convert milliseconds into hh:mm:ss format. final int hours = (int) (talkTime / (1000 * 60 * 60)); talkTime %= 1000 * 60 * 60; final int minutes = (int) (talkTime / (1000 * 60)); talkTime %= 1000 * 60; final int seconds = (int) (talkTime / 1000); final StringBuffer timeString = new StringBuffer(); if (hours > 0) { if (hours < 10) { timeString.append(0); } timeString.append(hours).append(':'); } if (minutes < 10) { timeString.append(0); } timeString.append(minutes).append(':'); if (seconds < 10) { timeString.append(0); } timeString.append(seconds); _talkTime = new BasicEditField("Talk Time: ", timeString.toString()); _talkTime.setEditable(false); } /** * Retrives a vector containing this displayer's fields. * * @return This displayer's fields. */ private Vector getFields() { final Vector fields = new Vector(); fields.addElement(_phoneNumber); fields.addElement(_talkTime); return fields; } } /** * Phone listener object. Listens for the callConnected, callDisconnected, * callHeld, and callResumed events and calculates talk time for each unique * phone number. */ private static final class ConcretePhoneListener extends AbstractPhoneListener { // Members ---------------------------------------------- // Helper object for searching the list of records private final PhoneNumberRecord _searchRecord = new PhoneNumberRecord( ""); // Maps call IDs to their phone numbers private final Hashtable _phoneNumberTable = new Hashtable(); /** * Default constructor */ ConcretePhoneListener() { // Not implemented } /** * Called when a phone call is connected. Finds the record with the * call's phone number (or creates a new one if one doesn't exist), * starts the "talk time" timer, and saves the record list. * * @param callId * The ID of the call that connected */ public void callConnected(final int callId) { final PhoneCall phoneCall = Phone.getCall(callId); final String phoneNumber = phoneCall.getDisplayPhoneNumber(); _phoneNumberTable.put(new Integer(callId), phoneNumber); PhoneNumberRecord record = getPhoneNumberRecordByPhoneNumber(phoneNumber); if (record == null) { // No record exists yet with this phone number, so create one // and put it in the list. record = new PhoneNumberRecord(phoneNumber); _model.addRow(record); } if (!record.isActive()) { record.start(); PhoneApiDemo.savePhoneNumberRecords(); } } /** * Called when a phone call is disconnected. Finds the record with the * call's phone number, stops the "talk time" timer, and saves the * phoneNumberRecord list. * * @param callId * The ID of the call that disconnected */ public void callDisconnected(final int callId) { final PhoneNumberRecord record = getPhoneNumberRecord(callId); // If an incoming phone call is ignored by the user rather than // answered, then // no record exists in _phoneNumberTable that matches callId. Thus, // record may // be null. If that's the case, do nothing; otherwise, proceed as // normal and // end the call. if (record != null) { _phoneNumberTable.remove(new Integer(callId)); if (record.isActive()) { record.end(); PhoneApiDemo.savePhoneNumberRecords(); } } } /** * Called when a phone call is put on hold. Finds the record with the * call's phone number, stops the "talk time" timer, and saves the * record list. * * @param callId * The ID of the call that was put on hold. */ public void callHeld(final int callId) { final PhoneNumberRecord record = getPhoneNumberRecord(callId); if (record.isActive()) { record.putOnHold(); PhoneApiDemo.savePhoneNumberRecords(); } } /** * Called when a phone call is resumed (taken off hold). Finds the * record with the call's phone number, starts the "talk time" timer, * and saves the record list. * * @param callId * The ID of the call that was resumed */ public void callResumed(final int callId) { final PhoneNumberRecord record = getPhoneNumberRecord(callId); if (!record.isActive()) { record.resume(); PhoneApiDemo.savePhoneNumberRecords(); } } /** * Retrieves a phone number record by call ID. Returns null if no such * record exists * * @param callId * The ID of the phone number record to retrieve * @return The phone number record, or null if no record matches callId */ private PhoneNumberRecord getPhoneNumberRecord(final int callId) { final String phoneNumber = (String) _phoneNumberTable.get(new Integer(callId)); return getPhoneNumberRecordByPhoneNumber(phoneNumber); } /** * Retrieves a phone number record by phone number. Returns null if no * such record exists. * * @param phoneNumber * The phone number of the phone number record to retrieve * @return The phone number record, or null if no record matches * phoneNumber */ private PhoneNumberRecord getPhoneNumberRecordByPhoneNumber( final String phoneNumber) { _searchRecord.setField(PhoneNumberRecord.PHONE_NUMBER, phoneNumber); final int index = _phoneNumberList.indexOf(_searchRecord); if (index != -1) { return (PhoneNumberRecord) _model.getRow(index); } return null; } } /** * This class implements a key listener for a screen so a menu is displayed * when the user presses ENTER. */ private static final class PhoneApiDemoKeyListener implements KeyListener { private final Screen _screen; /** * Creates a new PhoneApiDemoKeyListener object * * @param screen * The screen with to display the menu on */ public PhoneApiDemoKeyListener(final Screen screen) { _screen = screen; } // KeyListener methods // --------------------------------------------------------------------- /** * @see net.rim.device.api.system.KeyListener#keyChar(char,int,int) */ public boolean keyChar(final char key, final int status, final int time) { if (key == Characters.ENTER) { return _screen.onMenu(0); } return false; } /** * @see net.rim.device.api.system.KeyListener#keyDown(int,int) */ public boolean keyDown(final int keycode, final int time) { return false; } /** * @see net.rim.device.api.system.KeyListener#keyUp(int,int) */ public boolean keyUp(final int keycode, final int time) { return false; } /** * @see net.rim.device.api.system.KeyListener#keyRepeat(int,int) */ public boolean keyRepeat(final int keycode, final int time) { return false; } /** * @see net.rim.device.api.system.KeyListener#keyStatus(int,int) */ public boolean keyStatus(final int keycode, final int time) { return false; } } }