Rhodes
From Rhomobile
Introduction
Rhodes is a framework for building locally executing, device-optimized mobile applications. These applications are optimized for interacting with transactional enterprise application backends. It is also designed to work with synced local data using a local database such as SQLite or HSQLDB and a generic backend synchronization framework such as RhoSync (another open source component available from Rhomobile). It is available for iPhone, Research in Motion (Blackberry), Windows Mobile, Symbian and Android phones.
Rhodes takes much of its inspiration from web-oriented MVC style frameworks such as Ruby on Rails. However it has several simplifications and optimizations for the mobile scenario. As with developing apps on any good app framework, building a Rhodes mobile application will consist of building a set of files and putting them into the right directories for the respective models, controllers and views, as outlined in this tutorial
Sample Apps and Documentation
Here is a small conceptual and minimalist sample app that consists of SugarCRM cases, employees, and accounts.
Here is a small and minimalistic sample app that demonstrates use of system specific API-s such as access to phonebook, camera, GPS, etc.
Or see this tutorial for step by step instructions on how to build an application.
The latest RDoc is generated from source on each checkin and will be posted here.
Architecture
The following diagram illustrates the Rhodes platform and how it interacts with the RhoSync server:
Directory Structure
For example \sugar for an application that provides mobile access to SugarCRM. Beneath the app directory there will appear some .erb files. There will at least be an index.erb file as the default landing page. This default landing page will typically have links to the controllers for at least some of the data models. This index file is not associated with any controller. In case application needs default landing page to be associated with a controller, it is recommended to create a model/controller/view folder and use an action on this controller as a default start path. To do that, edit start_path property in the framework configuration file Please refer to the rhogen documentation for more information about the Rhodes directory structure.
Differences from Rails
The inspiration for the Rhodes application structure is the ease of use of web frameworks such as Rails and Merb. However we made some simplifications over the Rails directory structure. The most important changes and simplifications are:
- a model is implicitly defined by a named subdirectory in the /app folder
It is assumed that there is a synced data in the local store associated with this model.
- views files are in the model subdirectory
Maintaining the mapping between models and views is a little more difficult with distributed directories (e.g. subdirectories of the views subdirectory separate from the model directory). Hence all of the views files (index.erb, new.erb, create.erb, edit.erb, update.erb and new.rm) are placed right in the directory for the model.
- many other directories available are not present in the Rhomobile app directory structure
The Rails and Merb frameworks have subdirectories such as helpers, test, vendor, lib, log, db. Their equivalents are generally in the root directory of the application.
- lighter weight
Many of the differences from Rails are to make it easier to run on mobile devices with limited memory. Rhodes is lighter weight because it only providing the core necessary functions. Examples of the features that either aren't necessary in Rhodes or we haven't provided for space reasons are: web services, XML, pluralization, YAML, etc.
Controller Actions
Each Rhodes generated model controller has several actions to perform basic CRUD (create, read, update and delete) on the object. These actions are:
- index - to list all objects
- new - to display the editing form for creating a new object
- create - to actually create the object
- edit - to edit the actual object
- update - to perform the update of the object
- show - to view the object
- delete - to delete the object
This set of actions and the associated URL paths follows the Rails scaffolding pattern for creating CRUD actions for objects and the associated "map resources" convention for routing to those actions.
Model (Since 1.4)
Your model class is located in the generated model's folder. For the model "UserBase" the file would be called "user_base.rb"
Example of added upper_name method to the model:
class UserBase #add model specifc code here def upper_name self.name.upcase end end
Templates
We have referred to the template files used for views in the directory structure above. They all end in the extension .erb. Same as per Rails convention templates called index.erb (listing the data model objects), edit.erb (editing the object), show.erb (showing object values) and new.erb (creating a new object) are created by the rhogen generator. Inside the template, any valid HTML, javascript, and ruby can be used. Ruby code should be enclosed in <% and %> tags. See more information about ERB here.
Layouts
Rhodes supports a layout mechanism based on ERB templates. The default layout template is called "layout.erb" and is located in the application root folder. Unless overridden, this layout is rendered on all non-ajax requests.
Customizing Layouts
If you would like to override or customize layout behavior, you can call the render function with the following parameters:
render :action => 'index', :layout => 'mycustomlayout', :use_layout_on_ajax => false
The first argument is the action you would like to render. Next, the layout name is optionally provided which assumes the application root as a base directory. In the above example, rhodes would look for a "mycustomlayout.erb" in the application root directory (you also may call :layout => false to disable use of layout template). The use_layout_on_ajax argument tells rhodes whether or not to use the layout on ajax calls (default is false).
You can call layout method on controller to overwrite default layout name:
layout :mycustomlayoutThis will force render call to use mycustomlayout.erb in place of default layout file for all actions of this controller.
Menu Layout (since 1.2)
For menu-based platforms (currently RIM), the Rhodes framework provides the ability to change the native application menu items through the following simple api:
@default_menu = { "Item Label 1" => "/item1path", "Item Label 2" => "/item2path", ... } #=> overrides the rhodes default menu @menu = { "Item Label 1" => "/item1path", "Item Label 2" => "/item2path", ... } #=> overrides the default menu in a specific action
Default Menu
To change the default menu (in application.rb):
class AppApplication < Rho::RhoApplication def initialize super @default_menu = { "Go Home" => :home, "View Accounts" => "/app/Account", "Do Refresh" => :refresh, "Perform Sync" => :sync, "App Options" => :options, "View Log" => :log } end end
This will create a default menu with the following items (in top-down order):
Go Home
View Accounts
Do Refresh
Perform Sync
App Options
View Log
All of these menu items with the exception of "View Accounts" will call a reserved menu item. The "View Accounts" item when invoked will navigate to the path specified by the hash value, in this case /app/Account.
Controller Action Menu
To change the menu for a specific action (in controller.rb):
def index @accounts = Account.find(:all) @menu = { "Go Home" => :home, "Refresh" => :refresh, "Options" => :options, :separator => nil, "Log" => :log, "New Account" => "/app/Account/new" } render end
Note: The menu will reset to the application default menu as soon as the user navigates to a different action.
Reserved Menu Items
The following menu items are reserved and represented by symbols:
:home #=> navigate to configured start_path :refresh #=> refresh the current page :back #=> navigate to the previous page :sync #=> trigger SyncEngine.dosync :options #=> navigate to configured options_path :log #=> load the native logging UI :separator => nil #=> draw a separator line (if supported) :close #=> close or put rhodes to background (depending on platform)
The following is the default Rhodes menu if none is provided in application.rb:
@default_menu = { "Home" => :home, "Refresh" => :refresh, "Sync" => :sync, "Options" => :options, "Log" => :log, :separator => nil, "Close" => :close }
Native Bar Control (since 1.2.2)
On iPhone, Rhodes supports a tab-bar style layout in addition to the default toolbar-style layout. To use the tab-bar, all you have to do is define the tab items in your application.rb:
class AppApplication < Rho::RhoApplication def initialize # Tab items are loaded left->right, @tabs[0] is leftmost tab in the tab-bar @tabs = [{ :label => "Dashboard", :action => '/app', :icon => "/public/images/tabs/dashboard.png", :reload => true }, { :label => "Accounts", :action => '/app/Account', :icon => "/public/images/tabs/accounts.png" }, { :label => "Contacts", :action => '/app/Contact', :icon => "/public/images/tabs/contacts.png" }, { :label => "Options", :action => '/app/Settings', :icon => "/public/images/tabs/options.png" }] # Important to call super _after_ you define @tabs! super end end
Each tabbar item defined in the above sample defines three tab elements (all three are required):
:label #=> Visible label to display on the tabbar :action #=> Path to your rhodes action (i.e. '/app/Account' would load the Account index action) :icon #=> Relative path to tabbar item icon in your rhodes app (typically located in /public/images/) :reload => true #=> Optional argument which tells rhodes to reload the tab's :action, defaults to false
Behind the scenes, Rho::RhoApplication will detect the @tabs array in its initialize method and build the native bar through the following function:
NativeBar.create(bar_type, bar_item_array)
The following Native Bar Types are allowed:
RhoApplication::TOOLBAR_TYPE #=> 0 RhoApplication::TABBAR_TYPE #=> 1 RhoApplication::NOBAR_TYPE #=> 2
To disable the navbar entirely:
class AppApplication < Rho::RhoApplication def initialize @@toolbar = nil super end end
NOTE: Rhodes currently only supports up to 5 tabs in an application. An exception will be raised if you try to load more than 5 tabs at runtime.
Native bar runtime management (since 1.4)
Starting from version 1.4, Rhodes support creating and removing tab bars in runtime. Support of programmatic tab switching also added:
NativeBar.create(RhoApplication::TABBAR_TYPE, tabs) # Will remove existing toolbar/tabbar (if exists) and create new one NativeBar.remove # Will remove current toolbar/tabbar. Does nothing if there is no active bar NativeBar.switch_tab(1) # Will switch active tab to second (numeration is zero based i.e. 0 means first tab, 1 - second etc)
Note that after creating new toolbar or switching to another tab you need to call WebView.navigate method to get your webview correctly refreshed. For example:
NativeBar.create(RhoApplication::TOOLBAR_TYPE, nil) WebView.navigate('app/Settings')
or
NativeBar.create(RhoApplication::TABBAR_TYPE, tabs) NativeBar.switch_tab(3) WebView.navigate('app/Settings', 3)
Helpers
link_to
Examples of how to use link_to method:
link_to "Visit Other Site", "http://www.rhomobile.com/" ==> <a href=\"http://www.rhomobile.com/\" >Visit Other Site</a> link_to "Help", { :action => "help" } ==> <a href=\"/app/model/help\" >Help</a> link_to "Delete", { :action => "delete", :id => '{12}' } ==> <a href="/app/model/{12}/delete" onclick="var f = document.createElement('form'); f.style.display = 'none';this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;f.submit();return false;">Delete</a> link_to "Show", { :action => "show", :id => '{12}'},"style=\"height:4px;width:7px;border-width:0px;\"" ==> <a href="/app/model/{12}/show" style="height:4px;width:7px;border-width:0px;">Show</a> link_to "Delete", { :action => "delete", :id => '{12}' }, "class=\"delete_link\"" ==> <a href="/app/model/{12}/delete" class="delete_link" onclick="var f = document.createElement('form'); f.style.display = 'none';this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;f.submit();return false;\">Delete</a>" link_to "Invate",:action => :invite, :query => {:name => 'John Smith', 'address' => "http://john.smith.com"} ==> <a href="/app/model/invite?name=John%20Smith&address=http%3A%2F%2Fjohn.smith.com" >Invate</a>
url_for
Examples of how to use url_for method:
url_for '/some_url' ==> /some_url
When generating a new URL, missing values may be filled in from the current request's parameters. For example, if application name or model are not specifyed in the call parameters, they would be filled from the request.
url_for :action => :index
==> /app/model
url_for :action => :create
==> /app/model
url_for :action => :new
==> /app/model/new
url_for :action => :show, :id => '{12}'
==> /app/model/{12}/show
url_for :model => :another_model, :action => :show, :id => '{12}'
==> /app/another_model/{12}/show
url_for :controller => :another_controller, :action => :show, :id => '{12}'
==> /app/another_controller/{12}/show
url_for :application => :another_app, :model => :another_model, :action => :show, :id => '{12}'
==> /another_app/another_model/{12}/show
url_for :action => :create, :query => {:name => 'John Smith', 'address' => "http://john.smith.com"}
==> /app/model?name=John%20Smith&address=http%3A%2F%2Fjohn.smith.com
url_for :action => :show, :id => '{12}', :fragment => "an-anchor"
==> /app/model/{12}/show#an-anchor
Advanced Usage of Render
- Render does not need to be called at the end of each method. If render was not called, then it will default to rendering the action of the method you are in.
- Rendering of views works with no method in controller. If the method does not exist for an action, but a view exists for that action, then the view will be rendered.
- Rendering of files. render :file => "Settings/wait.erb" will render that file with the current controller's instance. By default layout is false when rendering a file
- Rendering of partials, with collections or locals. Either collections or locals must be provided. Only partials for the current controller is supported at this time.
render :partial => "ad", :collection => ["foo1","foo2","foo3"]
or
render :partial =>"ad", :locals => { :ad => "foo_ad" }
Will render the partial "_ad.erb" and the local variable "ad" will be available. With a collection, the partial will be rendered once per element.
Per Platform Files
On a per platform basis, you can use alternative files for any given file in your 'app' or 'public' folder. To do this, make a second file with the platform in the name of the file. For example:
default.css index.erb
Will be replaced with
default.android.css default.android.erb
When building for the android platform. This works for any file located within the 'app' or 'public' folder.
You must have have the base file exists for it to be replaced.
The valid platforms are: android, wm, bb, iphone
Device Capabilities / Native UI Elements
Rhodes will provide access to device specific capabilities such as GPS, PIM, camera, SMS, video player, accelerometer, proximity detector and native UI elements. Below is the device support matrix showing what release supports what device capabilities on a per device operating system basis.
| Capability | iPhone | Windows Mobile | BlackBerry | Symbian | Android |
|---|---|---|---|---|---|
| GeoLocation | 0.3 | 0.3 | 0.3 | 1.1 | 1.0 |
| PIM Contacts | 0.3 | 0.3 | 0.3 | 1.0 | 1.0 |
| Camera | 1.0 | 1.0 | 1.0 | 1.1 | 1.0 |
| Date/Time picker | 1.2.2 | tbd | 1.2 | tbd | 1.2 |
| Native Menu | tbd | tbd | 1.2 | tbd | tbd |
| Tabs / Tab Bar | 1.2.2 | tbd | tbd | tbd | tbd |
| Audio / Video capture | 1.4 | tbd | 1.4 | tbd | tbd |
| Bluetooth | 1.5 | tbd | 1.5 | tbd | 1.5 |
| Calendar Integration | 1.5 | tbd | 1.5 | tbd | 1.5 |
| Accelerometor | 1.5 | tbd | tbd | tbd | tbd |
| Push / SMS | 1.2 | 1.5 | 1.2 | tbd | 1.5 |
| Landscape | 1.5 | tbd | tbd | tbd | tbd |
| Native Maps | 1.4 | tbd | 1.4 | tbd | tbd |
GPS
Geolocation information is available as an asynchronous service through ajax call to predefined local URL or in a controller through ruby calls to GeoLocation class.
In general, GPS receiver consumes significant amount of energy and therefore it switched off most of the time in order to preserve phone battery. Any call to get location information will power up GPS receiver. However, it usually takes a while to acquire current position; in some cases, it may take several minutes and may not work at all indoors.
Asynchronous ajax calls
Rhodes framework provides predefined url call to which will return current known position. To use it, include the appropriate javascript library on your page:
/public/js/jquery-1.2.6.min.js and /public/js/rhogeolocation.js - for iPhone, Android, and Symbian /public/js/rhogeolocation-wm.js - for WM Blackberry webview control doesn't support ajax calls
Then add one of the following tags in the appropriate location in your HTML: <geolocation/>, <geolatitude/> or <geolongitude/>. Included javascript will query predefined url and fill these tags with location information.
<geolocation/> - returns a string in the form [formatted position];[latitude];[longitude]. For example: 37.3317° North, 122.0307° West;37.331689;-122.030731
<geolatitude/> - returns just the latitude
<geolongitude/> - returns just the longitude
GeoLocation class
GeoLocation.latitude #=> returns current latitude GeoLocation.longitude #=> returns current longitude GeoLocation.known_position? #=> returns true if system location system is up and acquired position
Send mail and call number urls
- mailto
<a href="mailto:@?subject=testing123">Mailto</a>
NOTES: even for empty address add @ symbol
- tel
<a href="tel:1-555-531-3255!8335033#!#!9582#">Tel</a> <a href="wtai://wp/mc;5195551212" title="Call">Work Tel</a> <a href="wtai://wp/mc;5195551213" title="Call">Home Tel</a>
NOTES: Here is the link to WML tel description: http://na.blackberry.com/eng/devjournals/resources/journals/oct_2004/wml_101.jsp
Sample
See controller and view in the /app/GeoLocation folder of the system API sample application for more information
- Testing on WM 6 Emulator
While developing your application on WM emulator, you may find FakeGPS utility usefull
- Testing on BB simulator
On the BB simulator select menu Simulate -> GPS Location.
- Providing Mock Location Data on Android
http://developer.android.com/guide/topics/location/index.html
PIM
Phonebook/Contacts
Rhodes provides access to the device's local phone book and stored the contacts provided by the Ruby class RhoContact.
The following methods are available on the RhoContact class:
Rho::RhoContact.find(:all) #=> return hash of hashes of all contacts stored in the phonebook. (index) Rho::RhoContact.find(@params['id']) #=> return hash of all properties of the contact identified by the provided id. (show) Rho::RhoContact.create!(@params['contact']) #=> create new contact in the phone book, set properties of the contact # from passed as parameter hash, and save created phonebook record. (create) Rho::RhoContact.update_attributes(@params['contact']) #=> find contact record in the phonebook, update record properties # from the has passed as parameter and save updated record. Contact id passed in the hash. (update) Rho::RhoContact.destroy(@params['id']) #=> remove contact identified by the provided id from the phonebook. (delete)
Contact's properties currently supported are following: "id","first_name","last_name","mobile_number","home_number","business_number","email_address","company_name"
On iPhone, contact's properties currently supported are following:
- General: "prefix", "first_name", "middle_name", "last_name", "suffix", "nickname", "birthday", "аnniversary" , 'created', 'updated', "spouse_name", "company_name", "job_title", "assistant_name", "assistant_number", 'personal_note'
(fields 'birthday', 'аnniversary', 'created', 'updated' expect date in the YYYY-MM-DD format)
- Addresses:
- 'street_address_1', 'city_1', 'state_1', 'zip_1', 'country_1'
- 'street_address_2', 'city_2', 'state_2', 'zip_2', 'country_2'
- 'street_address_3', 'city_3', 'state_3', 'zip_3', 'country_3'
(addres 1 is mapped to "work" address, 2 is to "home" address, 3 is to "other" address)
- Emails: "email_address", "home_email_address", "other_email_address"
- Phones: "business_number", "home_number", "mobile_number", "main_number", "pager_number", "home_fax", "work_fax"
- Home page: 'home_page'
Sample
For examples on how to use the API provided by this class, see view and controller in the /app/Contacts folder in the system API samples application.
Camera
Camera API allow to:
- take a picture
Camera::take_picture('/app/model/camera_callback')
- chose a picture from album
Camera::choose_picture('/app/model/camera_callback')
Once user take/choose picture, callback url you specified will be called. Callback is a POST message; body of the message contains status and image_uri.
- status may have following value: 'ok', 'cancel', or 'error'
- image_uri points to the taken/selected image stored in the /public/db-files folder; image file will have auto-generated name
Sample
See controller and view in the /app/Camera folder of the system API sample application for more information.
Date/Time picker (since 1.2)
Date/Time picker API allow to choose date or time:
DateTimePicker.choose(callback, title, initial_time, fmt)
or
DateTimePicker.choose(callback, title, initial_time, fmt, opaque)
There is the fourth parameter which value can be one of the 0, 1 or 2. Otherwise exception throws.
- 0 - mean full date and time input field
- 1 - date only input field
- 2 - time only input field
'opaque' is the optional parameter and should be string. It is non-interpreted (opaque) parameter which will be returned in callback as is.
Once user choose date/time and press OK or Cancel, callback url you specified will be called. Callback is a POST message; body of the message contains 'status', 'result' and, possibly, 'opaque'.
- 'status' may have following value: 'ok' or 'cancel'
- there is no 'result' in case if status is 'cancel'
- 'result' is the string containing number of seconds since Epoch and ruby time can be created from it using Time::at method
- 'opaque' - if exist, will contain passed to the method 'choose' data parameter.
NOTE: currently implemented for Android, iPhone and Blackberry
Sample
See controller.rb and index.erb view in the /app/DateTime folder of the system API sample application for more information. This example demonstrates each of the date/time picker types.
Ringtone manager (since 1.2)
Ringtone manager API allow to get list of installed ringtones and play any of them. Example:
@ringtones = RingtoneManager::get_all_ringtonesHere @ringtones will be a hash containing key/value pairs - user friendly name of ringtone and its full file name. This file name could be then passed to the method 'play':
RingtoneManager::play @ringtones['My Ringtone']
In case if 'play' called when another playing is active, it actually stop current playing and start new one.
RingtoneManager::stop
Method 'stop' stops playing at all. This method has no parameters and could be safely called even if there is no active playing.
NOTE: currently implemented for Android and Blackberry only. There is also known issue on Blackberry - only list of user installed ringtones accessible. System preinstalled ringtones are not accessible due to Blackberry limitations.
Sample
See controller and view in the /app/Ringtones folder of the system API sample application for more information.
Database (Rhom)
Rhom is a mini object mapper implemented in Ruby. It provides database-abstraction functionality to the Rhodes microframework. It allows simple models to be used with a "property bag" database.
Please see the Rhom page for more details.
Sources
Rhodes sources are also available by the rhodes ruby framework through the RhomSource object. You can use RhomSource just like a normal Rhom object with the exception that only the source_url field is writeable (below is the SourceController provided in the sample applications which demonstrates how to use RhomSource):
require 'rho/rhocontroller' require 'rhom/rhom_source' class SourceController < Rho::RhoController include Rhom def index @sources = RhomSource.find(:all) render :action => :index end def edit @source = RhomSource.find(@params['id']) render :action => :edit end def update @source = RhomSource.find(@params['source']['source_id']) @source.update_attributes(@params['source']) redirect :action => :index end end
You can also access the following attributes on RhomSource:
@source = RhomSource.find(@params['id']) puts @source.source_id #id of source puts @source.name #name of source puts @source.source_url #url of a source puts @source.last_updated #returns Time object of last sync for the source (in Time.at format) puts @source.last_inserted_size #returns number of records inserted on last sync puts @source.last_deleted_size #returns number of records deleted on last sync puts @source.last_sync_duration #returns duration of last sync for this source in seconds puts @source.last_sync_success #returns 1 if sync was successful, 0 otherwise puts @source.distinct_object #returns number of records
Show last sync time for Product source example:
<%= ::Rhom::RhomSource.find(Product.get_source_id).last_updated.strftime("%m/%d/%Y, %I:%M%p") %>
Database Crash Recovery
Rhodes provides the following functions for recovering the database from a bad or corrupt state
Rhom::Rhom.database_full_reset #deletes all records from object_values table and client_info table
Since 1.2, calling this method will also make a call to the "clientreset" action in rhosync. See the clientreset section for more information.
Image/Blob Sync
Synchronization of images to the rhosync server is provided automatically if your model handling camera images provides a source url.
Note: same mechanism may be used for any arbitrary files, not just images.
store image on client and synchronize it with backend server
Suppose you have the following model configuration:
Rho::RhoConfig::add_source("Image", {"url"=>"http://myrhosync.com/apps/cameratest/sources/image/", "source_id"=>35})
Then you have a camera callback function defined in the Image model's controller:
def camera_callback if @params['status'] == 'ok' #create image record in the DB image = Image.new({'image_uri'=>@params['image_uri']}) image.save SyncEngine.dosync end #reply on the callback render :action => :ok, :layout => false end
This callback will trigger a sync after saving the image, which will subsequently push it up to the rhosync server. It is up to your image source adapter to handle the image (which is provided to you as a paperclip attachment called "blob":
def create(name_value_list,blob) if blob obj = ObjectValue.find(:first, :conditions => "object = '#{blob.instance.object}' AND value = '#{name_value_list[0]["value"]}'") path = blob.path.gsub(/\/\//,"\/#{obj.id}\/") name = name_value_list[0]["value"] `cp #{path} #{File.join(RAILS_ROOT,'public','images',name)}` end end
In this example, we copy the blob to the rails public folder so that we can query for the list of images in the query method. See the "camera" sample source adapter that comes with rhosync.
fetch image/blob file from the backend server
In your query call, you should store the url to the new backend image so that the device can fetch the image when it syncs for the new records:
def query @result={} Dir.entries(PATH).each do |entry| new_item = {'image_uri' => BASEURL+'/images/'+entry, 'attrib_type' => 'blob.url'} unless entry == '..' || entry == '.' || entry == '.keep' p "Found: #{entry}" @result[entry.hash.to_s] = new_item end end @result end
See rhosync/vendor/sync/SystemApiSamples/camera.rb for a complete rhosync adapter example.
Sample
See controller and view in the /app/Blob folder of the system API sample application for more information.
RhoError class
This class contains error codes and method message() to translate error code to text message. All callbacks return error code from this class. Status text in callback contain some internal error, so in most cases it should not be exposed to user. Currently RhoError contains the following error codes:
ERR_NONE = 0 ERR_NETWORK = 1 ERR_REMOTESERVER = 2 ERR_RUNTIME = 3 ERR_UNEXPECTEDSERVERRESPONSE = 4 ERR_DIFFDOMAINSINSYNCSRC = 5 ERR_NOSERVERRESPONSE = 6 ERR_CLIENTISNOTLOGGEDIN = 7 ERR_CUSTOMSYNCSERVER = 8 ERR_UNATHORIZED = 9
Sample
See Login/Logout Manager as an example.
Login/Logout Manager
Rhodes comes with a built-in login manager for handling authentication with RhoSync. The following functions are available to access the login manager:
# Pre 1.2 SyncEngine::login(@params['login'], @params['password']) #returns 1 if login was successful to all sources, otherwise 0 SyncEngine::logout #performs a logout on each source SyncEngine::logged_in #returns 1 if true, 0 if false
Please see the "Settings" controller in your rhodes application for an example of how this is used.
Since 1.2 Rhodes uses a callback for login and the function has changed accordingly:
# Since 1.2 use this login method SyncEngine.login(@params['login'], @params['password'], (url_for :action => :login_callback)) # Default callback that's provided by the generator def login_callback err_code = @params['error_code'].to_i if err_code == 0 # run sync if we were successful WebView.navigate Rho::RhoConfig.start_path SyncEngine.dosync else if errCode == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] end if !@msg || @msg.length == 0 @msg = Rho::RhoError.new(errCode).message end WebView.navigate ( url_for :action => :login, :query => {:msg => @msg} ) end end
Sample
See controller and view in the /app/Settings folder of the system API sample application for more information.
Configuration
Each rhodes application contains a configuration file called "rhoconfig.txt". A typical rhoconfig.txt will look like the following when an app is generated:
# Startup page for your application start_path = '/app' # Path to the options page (in this case handled by javascript) options_path = '/app/Settings' # Location of bundle url (i.e. from rhohub.com); used by desktop win32 simulator rhobundle_zip_url = '' # Optional password to access bundle (usually not required); used by desktop win32 simulator rhobundle_zip_pwd = nil # Rhodes log properties MinSeverity = 0 LogToOutput = 1 LogCategories = * ExcludeLogCategories = # Keep track of the last visited page KeepTrackOfLastVisitedPage = 0 LastVisitedPage = '' # Sync server url. This must be the full path to your sources. For example: http://rhomobile.rhohub.com/apps/SugarCRM/sources/ syncserver = 'http://myrhosyncserver.com/apps/MyNewApp/sources/'
Last Visited Page
Rhodes may keep track of the last visited page so next time you start your application framework will navigate to this page. To enable this feature, use KeepTrackOfLastVisitedPage=1
NB: Keep in mind, POST requests will be converted to GET-s.
Application database version
If you want next version of your application clean database from previous one and start form clean db: set app_db_version in rhoconfig.txt. When rhodes started it compare db version with app_db_version and clean db if they do not match.
Ruby API
Each of the configuration options shown above are available through the RhoConfig ruby api:
Rho::RhoConfig.options_path #=> returns '/app/Settings' Rho::RhoConfig.options_path = '/app/MyObject' #=> assignment will be saved to rhoconfig.txt
Note: you may store any arbitrary options in the rhoconfig using ruby API. Just keep in mind, they are going to be lost next time user install new version of the application.
SyncEngine Configuration (since 1.2)
In the previous section, there is a "syncserver" option available. This option can be changed through the Rho::RhoConfig api as you would expect. However, since changing the syncserver requires updating the SyncEngine at runtime, another ruby call is available:
SyncEngine.set_syncserver('http://examplesyncserver.com/apps/MyApp/sources/') #=> nil
Calling the function above will do the following:
- update in-memory syncserver for the SyncEngine (all following synchronization will use the new syncserver)
- perform a logout to make sure session is removed for old syncserver
- write the new syncserver property to rhoconfig.txt
To disable auto sync you can call
SyncEngine.set_pollinterval(0)
OR set in rhoconfig.txt: sync_poll_interval=0
Logging
To show application log on the device using:
Rho::RhoConfig.show_log
To send log to sync server:
Rho::RhoConfig.send_log
Persistent storage (Blackberry only, since 1.2)
To enable using of RIM persistent storage API in rhodes, just add line
use_persistent_storage = 1
to your rhoconfig.txt. It will enable using of RIM persistent storage (instead of files) to store DB version, DB journal and database itself. The main goal of persistent storage is that all objects stored there by application will be automatically removed when application uninstalled.
SyncEngine class
- SyncEngine.dosync(show_sync_status) # requests to start sync process; if optional parameter show_sync_status set to false, then sync status popup will not be displayed (default is true)
- SyncEngine.dosync_source(source_id, show_sync_status) # requests to start sync process on the specified source; if optional parameter show_sync_status set to false, then sync status popup will not be displayed (default is true)
- SyncEngine.lock_sync_mutex # wait to acquire sync engine lock (useful for performing batch operations)
- SyncEngine.unlock_sync_mutex # release acquired sync engine lock (make sure you do this if you call lock!)
- SyncEngine.login(login, password, callback) # authenticates user with sync server; cllback url will be called after login operation is completed, more info here
- SyncEngine.logged_in # returns true if user is authenticated by the sync server, more info here
- SyncEngine.logout # logout user from sync server, more info here
- SyncEngine.stop_sync # stops any sync operations in progress
- SyncEngine.set_notification(source_id, url, params) # more info here
- SyncEngine.clear_notification(source_id) # more info here
- SyncEngine.set_pollinterval(interval) # set sunc poll interval; 0 will disable polling for updates, sync still may be initiated by PUSH notifications
- SyncEngine.set_syncserver(syncserver) #set sync server address and stores it in Rho Config
- SyncEngine.set_objectnotify_url # more info here
- SyncEngine.set_pagesize - set page size for server show request
- SyncEngine.get_pagesize - set page size for server show request
Sync notification
Once sync happened on a source view which reflects data provided by the source should be refreshed. To accomplish that you may use sync notification mechanism.
To set a notification use one of the following ruby calls:
Account.set_notification(url,params) SyncEngine.set_notification(source_id,url,params) SyncEngine.set_notification(-1,url,params) #set callback for all sources
For example:
Account.set_notification("/app/Account/sync_notify", "sync_complete=true")
Once object(s) associated w/ Account source are changed, the view will be directed to sync_notify action (with params 'sync_complete=true') if user happens to be on the same page.
There 3 status in sync callback: Common parameters: source_id, source_name
- in_progress. Parameters: total_count, processed_count, cumulative_count. You can use it to report sync progress to user.
- error. Parameters: error_code, error_message
- ok. Parameters: total_count, processed_count, cumulative_count
def sync_notify status = @params['status'] ? @params['status'] : "" if status == "error" errCode = @params['error_code'].to_i if errCode == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] else @msg = Rho::RhoError.new(errCode).message end WebView.navigate(url_for(:action => :server_error, :query => {:msg => @msg})) elsif status == "ok" if SyncEngine::logged_in > 0 WebView.navigate "/app/Page" else # rhosync has logged us out WebView.navigate "/app/Page/authenticate_form" end end end
Note: if view updated using ajax calls, this mechanism may not work correctly as view location will not change from one ajax call to another.
Create objects errors
Server can return errors while creating objects. In this case sync notification callback receive the following parameter:
create_error - array of hashes. Each hash contain 'object' and 'error_message'.
Sync object notification
Application can receive notifications about object modifications by sync engine. So application can update current page if it displays modified object.
- Call SyncEngine.set_objectnotify_url. You can call it once for example in AppApplication.initialize
- In the controller method call add_objectnotify with array of objects or single object before render method.
- If you use url_for in your erb-file: when select specific object to view, url contain object id, so this object id will added to notification map automatically.
- Notification callback receive 3 arrays of hashes- 'deleted' , 'updated' and 'created'. Each hash contain 'object' and 'source_id'
Example
class AppApplication < Rho::RhoApplication def initialize super SyncEngine::set_objectnotify_url("/app/Settings/sync_object_notify") end end class ProductController < Rho::RhoController #GET /Product def index @products = Product.find(:all) add_objectnotify(@products) render end end def sync_object_notify #do something with notification data WebView.refresh end
Asynchronous "ask" call
If your rhosync source adapter supports the "ask" call to directly query the backend, you can use the following function in your application to trigger an "ask" call to rhosync:
Account.ask('bigco')
This will trigger a call to the ask method on the Account source on rhosync with params['question'], which returns a set of object_values back to the client.
Asynchronous search call
If you have big database on the server you don't have to load it to device. In such case you can use asynchronous search feature:
Somewhere in your controller:
Contact.search( :from => 'search', :search_params => { :FirstName => @params['FirstName'], :LastName => @params['LastName'], :Company => @params['Company'] }, :offset => page*page_size, :max_results => page_size, :callback => '/app/Contact/search_callback', :callback_param => "" )
Rhodes 1-4 Note: Rhodes automatically add search_params to callback_param, so in callback it is available as:
@params['search_params'][:FirstName] etc
Your callback would look something like this:
def search_callback if (status && status == 'ok') WebView.navigate ( url_for( :action => :show_page, :query => @params['search_params']) ) end #TODO: show error page if status == 'error' render :action => :ok end
To make find faster use : http://wiki.rhomobile.com/index.php/Rhom#find.28.2Aargs.29_Advanced_proposal
And after callback is called and data is ready, you may render next page:
def show_page $contacts = Contact.find(:all, #:conditions => ["LOWER(FirstName) LIKE ? OR LOWER(LastName) LIKE ? OR LOWER(Company) LIKE ?", # @params[:FirstName], @params[:LastName], @params[:Company]] :conditions => { {:func=>'LOWER', :name=>'FirstName', :op=>'LIKE'}=>@params[:FirstName], {:func=>'LOWER', :name=>'LastName', :op=>'LIKE'}=>@params[:LastName], {:func=>'LOWER', :name=>'Company', :op=>'LIKE'}=>@params[:Company], }, :op => 'OR', :select => ['FirstName','LastName', 'Company'], :per_page => page_size, :offset => page*page_size ) render :action => :show_page end
To stop call callback return 'stop':
def search_callback if (status && status == 'ok') WebView.navigate ( url_for :action => :show_page ) end #TODO: show error page if status == 'error' 'stop' end
Asynchronous search call parameters
Search call takes hash of the following parameters. All parameters not named one of those below are assumed to be search parameters.
:from #=> sets the path or custom method that records will be fetched from (optional, default is 'search') :offset #=> starting record to be returned :max_results #=> max number of records to be returned :callback #=> callback to be called after search is completed :callback_param #=> parameters to be passed to the callback (optional)
- progress_step #=> optional parameter, define how often search callback will be called with 'in_progress' state
Call from example above will result in the following call to the RhoSync server:
/search?conditions[FirstName]=Jon&conditions[LastName]=Smith&conditions[Company]=Acme,offset=30,max_results=10
AsyncHttp (since 1.4.1)
To make asynchronous calls to web service or any other http server, you may use RhoHttp:
- get(:url, :headers, :callback,:callback_params)
- post(:url, :headers, :body, :callback,:callback_params)
- cancel # cancel current http call
AsyncHttp.get( :url => 'http://www.example.com', :headers => {'Cookie' => cookie}, :callback => (url_for :action => :httpget_callback), :callback_param => "" ) AsyncHttp.post( :url => 'http://www.example.com', :headers => {'Cookie' => cookie}, :body => 'Test', :callback => '/app/Contact/httpget_callback', :callback_param => "" ) def httpget_callback #@params contain response. In case of json or xml (ContentType=application/json or application/xml respectively) @params['body'] #contain parsed body represented as hash of hashes #in case of unrecognized body type @params['body'] contain raw text body #in case of error usual RhoError info returned plus @params['http_error'] and @params['body'] contain server response end
PUSH notifications (since 1.2)
As of 1.2 release of Rhodes, PUSH support implemented on Blackberry and iPhone
Blackberry
Notifications to BB sent using PAP 2.0 message through BES/MDS server.
To listen for incoming messages on BB application will start when device powered up and run listener thread in background all the time. Use "push_port" option in the rhoconfig.txt to specify port on which client will listen for incoming push messages. If port is not specified, default will be 100.
iPhone
iPhone PUSH support uses Apple Push Notification service introduced in iPhone SDK 3.0
Payload
It is possible to combine more then one operation in a single message.
There are no required operations. There are default operations - if operation is not specified, no default operation will be performed.
In case of Blackberry if application in background, show_popup operation will bring application upfront; other operations will not. In case of iPhone, regardless of operation, user will be presented with option to activate application if it is not running.
Payload may include following operations which client will perform as it receive PUSH message:
- do_sync
do_sync = http://source-url/1/
It will do sync on spec specified sync source; use "all" to sync all sources
- show_popup
show_popup = "Some message"
It will bring app upfront and show specified message
- vibrate
vibrate = duration in milliseconds
It will vibrate for the specified number of milliseconds, up to 25500; if 0 or no duration is specified, it will vibrate for 2500 millisecond.
- play_file
play_file = file-name.ext, [media-type]
It will play specified file if media type supported by the phone. iPhone will ignore media-type parameter.
File should be included to the application bundle.
In case of Blackberry, if file is in public folder, file name will be /apps/public/test-file.mp3 Media type should be either specified explicitly or may be recognized from file extension. Known file extensions are: .mp3 - audio/mpeg; .wav - audio/x-wav
In case of iPhone, audio files should be placed in the /public/alerts folder and build script will copy them into root of the application main bundle (iPhone wouldn't play file from any other place).
Alerts (since 1.2)
In your controller, you may call on system alert methods to show popup, vibrate, or play audio file.
- show_popup
Alert.show_popup "Some message"
It will bring app upfront and show specified message
- vibrate
Alert.vibrate(duration_in_milliseconds)
It will vibrate for the specified number of milliseconds, up to 25500; if 0 or no duration is specified, it will vibrate for 2500 millisecond.
- play_file
Alert.play_file(file_name.ext, media_type)
It will play specified file if media type supported by the phone. File should be included to the application. For example, if file is in public folder, file name will be /apps/public/test-file.mp3 Media type should be either specified explicitly or may be recognized from file extension. Known file extensions are: .mp3 - audio/mpeg; .wav - audio/x-wav
Sample
See controller and view in the /app/Alert folder of the system API sample application for more information.
WebView class
It is possible to call on the WebView (browser) directly from your controllers. This API is recommended for use from callbacks, such as sync callback or camera callbacks.
WebView.refresh #=> will force WebView refresh current page WebView.navigate(url) #=> will force WebView navigate to provided location (url) # Since 1.2.2 WebView.navigate supports an optional index parameter (defaults to 0, useful for tabbed applications) WebView.navigate(url, index) WebView.current_location #=> will return location (url) of the currently displayed page WebView.execute_js(js) #=> will try to execute javascript string in the context of the currently displayed page; iPhone only WebView.active_tab #=> returns index of @tabs array for currently selected tab; iPhone only, all other platforms it returns 0
Sample
See controller and view in the /app/Camera folder of the system API sample application for some of the examples of how to use WebView in callbacks.
MapView
MapView class provides an embeddable map interface, similar to the one provided by the Maps application. The following code would go into your controller and the map appears on a whole page.
map_params = { :settings => {:map_type => "hybrid",:region => [@params['latitude'], @params['longitude'], 0.2, 0.2], :zoom_enabled => true,:scroll_enabled => true,:shows_user_location => false, :api_key => 'Google Maps API Key'}, :annotations => [{:latitude => @params['latitude'], :longitude => @params['longitude'], :title => "Current location", :subtitle => ""}, {:street_address => "Cupertino, CA 95014", :title => "Cupertino", :subtitle => "zip: 95014", :url => "/app/GeoLocation/show?city=Cupertino"}, {:street_address => "Santa Clara, CA 95051", :title => "Santa Clara", :subtitle => "zip: 95051", :url => "/app/GeoLocation/show?city=Santa%20Clara"}] } MapView.create map_params
Map settings:
- map_type - widget may display maps of three types: standard, satellite, and hybrid
- region - [latitude,longitude,latitudeDelta,longitudeDelta]. The area currently displayed by the map view. The region encompasses both the latitude and longitude point on which the map is centered and the span of coordinates to display. The span values provide an implicit zoom value for the map. The larger the displayed area, the lower the amount of zoom. Similarly, the smaller the displayed area, the greater the amount of zoom.
- latitude,longitude - map coordinate of the region center
- latitudeDelta - the amount of north-to-south distance (measured in degrees) to display on the map. Unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always approximately 111 kilometers (69 miles).
- longitudeDelta - the amount of east-to-west distance (measured in degrees) to display for the map region. The number of kilometers spanned by a longitude range varies based on the current latitude. For example, one degree of longitude spans a distance of approximately 111 kilometers (69 milies) at the equator but shrinks to 0 kilometers at the poles.
- zoom_enabled - true if zoom of the map is enabled
- scroll_enabled - true if scrool of the map is enabled
- shows_user_location - true if current user location is displayed on the map
- api_key - Google Maps API Key (sign up for it here: http://code.google.com/apis/maps/signup.html)
Annotations - array of map annotation objects (list of pins on the map). Annotation:
- latitude,longitude - map coordinate of the annotation
- street_address - if map coordinate is not specified, framework will attempt to obtain it using provided street address from google geo-coding service
- title - title of the annotation callout
- subtitle - subtitle of the annotation callout
- url - url to follow when user click on the callout button
Sample
See GeoLocation/controller.rb of system API sample application for some of the examples of how to use MapView class.
File system access
Rhodes client file system structure
<rhodes root> #system-dependent path
apps #Rho::RhoApplication::get_base_app_path
app #Rho::RhoApplication::get_app_path('app') - contain models
model1 #Rho::RhoApplication::get_model_path('app','model1')
public #contains files from application public folder
db-files #contains files stored in database(blobs)
db #contains schema and data files
lib #contains rho framework library files. Blackberry does not has this folder, library files are stored in jar
RhoLog.txt #application log
Read\write file example
fileName = File.join(Rho::RhoApplication::get_base_app_path(), 'myfile.txt') File.open(fileName).each do |line| end fileNameW = File.join(Rho::RhoApplication::get_base_app_path(), 'tempfile.txt') f = File.new(fileNameW) f.write('test') f.close
Platform notes
Blackberry
- Only read from files are supported as of 1.4 release
- Simulator files folder (4.6 and bigger) - <sdk root>/components/simulator/sdcard/rho/<appname>
- Device files folder can be found using Media/Explore(see http://wiki.rhomobile.com/index.php/Building_Rhodes_Apps_on_Supported_Platforms#Application_log_on_the_device)
iPhone
- Simulator files folder - run search for RhoLog.txt from the drive root. Files are placed inside simulator folder.
Windows Mobile
- Device/simulator files folder root - Program Files/<app name>/rho
System class
System ruby class provides access to system specific information.
System.get_property(property) #=> returns value of a named system property. # At the moment only property available is 'platform', which will return Apple, Blackberry, or WM. Later, # it will be extended with other properties which will detail system capabilities, such as availability of camera, or gps, or other # system specific properties. System.has_network #=> returns true if phone has network connection # Due to capabilities of particular OS-es this call implemented only for iPhone. # Developers MUST use this on iPhone to pass on the AppStore.
Sample
See layout.erb of system API sample application for some of the examples of how to use System class.
Advanced Topics
Supported extensions and libraries
To keep rhodes lightweight we didn't provide extensions and libraries beyond core Ruby functionality. Examples of the features that either aren't necessary in Rhodes or we haven't provided for space reasons are: web services, XML, pluralization, YAML, etc.
Our C/C++ implementation based on original Ruby C code, 1.9 release.
Our Java implementation is based on XRuby, which supports Ruby 1.8 (We didn't use JRuby because it is substantially bigger and required version of java which is not available on most of the target mobile platforms).
Both implementations support such core classes and module as:
BasicObject, Object, Module, Class, Integer, Float, Numeric, Bignum, Rational, Complex, Math, String, StringScanner, StringIO, Array, Hash, Struct, Regexp, RegexpError, MatchData, Data, NilClass, TrueClass, FalseClass, Comparable, Enumerable, Enumerator, Converter, Marshal, IO, Dir, Time, Date, Signal, Mutex, Thread, ThreadGroup, Process, Fiber, FiberError, Method, UnboundMethod, Binding, RubyVM, GC, Exception, SystemExit, fatal, SignalException, Interrupt, StandardError, TypeError, ArgumentError, IndexError, KeyError, RangeError, ScriptError, SyntaxError, LoadError, NotImplementedError, NameError, NoMethodError, RuntimeError, SecurityError, NoMemoryError, EncodingError, CompatibilityError, SystemCallError, Errno, ZeroDivisionError, FloatDomainError, IOError, EOFError, ThreadError
We are in the process of adopting rubinius specs to test ruby compatibility across different platforms.
Localization
See rhodes-system-api-samples as an example.
Rhodes use localization_simplified library to support non-English languages.
Add to build.yml:
extensions: ["rholang"]
Note: in case of several extensions, insert space after extension name and comma:
extensions: ["rholang", "net-http"]
Create utf-8 encoded file in app :
<app_folder>/app/lang/lang_<lang_id>.rb
This file will be automatically loaded by rhodes based on current locale. For Example create lang_en.eb:
module Localization
Views = {
:greeting => "This is test"
}
end
And use this string in the view:
- <%= Localization::Views[:greeting] %>
Details
- All non-ascii symbols should be utf-8 encoded
- To get current locale on the phone use System.get_locale method. It returns 'en', 'de' etc locale id's
- To show localized Date and Time:
Time.now.to_formatted_s(:long)
Time.now.strftime("%B %d, %Y %H:%M") # all names will be localized
Date.today.to_formatted_s(:long)
Date.today.strftime("%B %e, %Y") # all names will be localized
- To show currency:
Rho::NumberHelper.number_to_currency #see rails analog for details
net/http library support
Add to build.yml:
extensions: ["net-http"]
Note: in case of several extensions, insert space after extension name and comma:
extensions: ["rholang", "net-http"]
Ruby code example:
require 'net/http' Net::HTTP.get_print 'www.example.com', '/index.html'
See also rhodes-system-api-samples
json library support
Add to build.yml:
extensions: ["json"]
Note: in case of several extensions, insert space after extension name and comma:
extensions: ["json", "net-http"]
Ruby code example:
require 'json'
JSON.parse("[{\"count\":10}]")
See also rhodes-system-api-samples
rexml library support
Add to build.yml:
extensions: ["rexml", "set"]
Ruby code example:
require 'rexml/document'
file = File.new("bibliography.xml")
doc = REXML::Document.new(file)
puts doc
Detailed notes on Ruby standard library support
For iPhone the Date class is supported
require 'date' puts Date.today.to_s
For Blackberry Date is still not supported. Use this instead:
Time.now.strftime('%Y-%m-%d')
Adding Libraries to Your Rhodes Application
During the course of your app development you might need to add an external ruby library with extra features that the rhodes framework doesn't provide. While we don't guarantee that all ruby libraries will work on the mobile device, you can follow the steps below to add and test libraries as needed.
In Rhodes, the require path is relative to the "app" subdirectory, since this is what gets bundled with the rhodes client.
- Assuming your application is called "mynewapp", create a directory under app called lib (or whatever you wish to call it):
cd mynewapp mkdir app/lib
- Add your ruby files to this directory:
cp /path/to/my_lib.rb app/lib/my_lib.rb
- Now, in your application (controller.rb for example), you can load this library like the following:
require 'rho/rhocontroller' require 'lib/my_lib' class TicketController < Rho::RhoController def use_lib @a = MyLib.new ... end end
- Please note that "rubygems" are not loaded on the device Ruby VM because of size constraints, therefore all third-party ruby library source files must be put into the lib directory as described above.
Adding Libraries to Rhodes Framework
There are two ways to add Ruby libraries to the Rhodes framework, essentially dependent upon how you choose to build your Rhodes application.
If you are using Rhodes via the RubyGems installation, you must add external Ruby libraries to your RubyGems installation directory for the 'rhodes-framework' gem. Your RubyGems installation directory can be found with `gem env` in a terminal.
- For example, a user on Linux might place additional libraries in the following directory:
/usr/local/lib/ruby/gems/1.8/gems/rhodes-x.x.x/lib/framework
- Similarly, a user on Mac OSX 10.5 might place them here:
/Library/Ruby/Gems/1.8/gems/rhodes-x.x.x/lib/framework
- For Windows, this location might be:
C:/ruby/lib/ruby/gems/1.8/gems/rhodes-x.x.x/lib/framework
- If you are using a clone of the Rhodes Git repository, you can put additional libraries in the following directory (preferably on your own github fork):
<rhodes-clone>/lib/framework
Including the library into your application is simple once the library is in the proper directory.
- Assuming the library has been added to the correct location, require the library from a controller in your Rhodes application:
require 'libname'
You can now use the added library to access additional functionality not provided by the Rhodes framework.
NOTE: Once again, it should be mentioned that not all libraries are guaranteed to work with Rhodes.
BlackBerry Browser Suport
Check out the BlackBerry Browser Version 4.2 Content Developer Guide for the HTML, CSS and JavaScript that is supported. http://docs.blackberry.com/eng/deliverables//1143/browser_devguide.pdf
Testing/Logging/Reporting/Profiling
Logging
There are two methods to log messages.
From any controller you can log using the methods app_info and app_error. These methods take a string and will write that to rholog.txt with the category of your controllers name.
There also is a logging class called RhoLog. This class has methods info and error which take 2 strings. The first string is the category, the second string is the message.
In rholog.txt the lines appear as follows:
<Timestamp> <category> | <message>
Debugging
You can debug your Rhodes app running on OSX using the Rhodes Debugger
Testing
http://wiki.rhomobile.com/index.php/Testing

