Using Invocation Framework in Adobe AIR

by Developer on ‎12-10-2012 10:24 AM (4,569 Views)

Symptoms

Using the invocation framework in Adobe® AIR® for BlackBerry® 10 devices can make your app integrate with other apps seamlessly, providing the end user an integrated environment.  The Invocation Framework for the AIR SDK is a powewrful tool that makes integration simple.

Diagnosis

Here is a tutorial to make some of the ins and outs of the SDK a little easier.

Solution

This tutorial is meant to give a step-by step usage of the invocation framework for BlackBerry 10 devices.  It is not meant to be a detail usage of the API, but to provide sufficient information to implement the invocation framework for both client and server.

 

Server applications are called “targets” in the invocation framework.  Targets have to define or register themselves to the invocation framework so that client applications can know about them and their capabilities.

 

REGISTER TARGET

There are two main types of targets. “Applications” and “Viewers”.  If your app will be a target to be invoked as an application, then you need to register your app as a type “APPLICATION”.  If your app will only be a viewer (embedded in a client app), then you need to register your app as a type “VIEWER”.  If your app will be both invoked as an application and have the capability to be embedded as a viewer, then you have to register as both.

 

Register as an APPICATION

To register as an application, you need to add the following to your bar-descriptor.xml file:

  <invoke-targetid="your.reverse.dns.app.id.app">

    <type>APPLICATION</type>

       <filter>

             <action>bb.action.OPEN</action>

             <mime-type>application/x-app-type</mime-type>

       </filter>

  </invoke-target>

The ID in the invoke-target node has to be a unique ID in the file.  This can be the same ID as your application ID defined in your –app.xml file, but it does not have to be.  This ID registers your app to allow a client app to directly invoke your app or received during a target query request. Each invoke-target section needs to have a “type” and a “filter”.  The “type” in this case is “APPLICATION” because you are registering your app to be invoked as an application.  The “filter” sections defines additional parameters in what can the target application can do.  In this case, the filter has an action to be “opened” by a client application.

 

Register as a VIEWER

To register as an application that can be a viewer in a client application, you need to add the following to your bar-descriptor.xml file:

  <invoke-targetid="your.reverse.dns.app.id.viewer">

    <type>VIEWER</type>

       <filter>

             <action>bb.action.OPEN</action>

             <mime-type>application/x-app-type</mime-type>

       </filter>

  </invoke-target>

Similar to the APPLICATION registration, you need to give a unique ID to the invoke-target section.

 

NOTE: If you are both an APPLICATION and a VIEWER, the IDs in both invoke-target sections have to be unique.  You will get an error if they are the same.  Since it is not a great idea to change the ID after you release the application, it is best to make the IDs unique between invoke-target sections from the very beginning.  Suggest appending the APPLICATION ID with “.app” and the VIEWER ID with “.viewer” as shown above.  You could probably add another invoke-target section with yet another ID if you add viewer functionality later, but you may as well as assume that you will at some point and have the invoke-target IDs unique in preparation of having more than one invoke-target sections.

 

MIME TYPES

The mime for each section will describe the category of application that your application is.  This will allow the plug and play of application on the device.  So if your application falls under the category of calculators, contacts, calendars, picture viewer, mapping, etc., then you want your application mime-type to be the same as the RIM® provided applications.  If your app is in its own category of apps that are not providing by RIM, then you can make up your own “extended” application mime-type.

 

If your application is in a unique category of applications and you have competition, you may want to reach out to them so that there can be an agreed upon mime type so that other applications can invoke your category of applications.  Then your application will be able to compete on features and price and let the consumer decide which target application is best suited for them.  Allow the consumer to plug and play target applications to best suit their individual needs.

 

Register supported file types

A target application can also register the types of files it supports.  This will allow the file browser (or client applications that is managing a certain file) to open files directly into an application that supports various file extensions.

 

  <invoke-targetid="com.o2interactive.magellan.compass.app">

    <type>APPLICATION</type>

       <filter>

             <action>bb.action.OPEN</action>

             <mime-type>text/csv</mime-type>

             <mime-type>text/xls</mime-type>

             <propertyvar="uris"value="file://"/>

             <propertyvar="exts"value="csv,xls"/>

       </filter>

  </invoke-target>

 

 

TARGET START TYPE

When your application gets started, it can get started in one of three ways:

1)      Opened (invoked) by the user from the desktop

2)      Opened (invoked) by another application and it shows up to the user as if they opened it up by themselves.  This may or may not have a message delivered to it to allow the app to perform some initial function.  For example, open up a file.

3)      As a viewer, embedded in the requesting, client application.  As with the invoked by app type above, this too can send an initial message to the app to perform some initial function.

 

When a target app is first created, it its constructor, one of the very first things it needs to do is listener for the invocation event.

 

 

InvokeManager.invokeManager.addEventListener(InvokeEvent.INVOKE, Invoked );

 

 

The event listener would then be something like this:

 

//////////////////////////////////////////////////////////////////////

private function Invoked( event : InvokeEvent ) : void

{

        InvokeManager.invokeManager.removeEventListener(InvokeEvent.INVOKE, Invoked );

                       

        var startup : int = InvokeManager.invokeManager.startupMode;

        switch( startup )

        {

                case InvokeStartupMode.INVOKE : this.appInvoked(); break;

                case InvokeStartupMode.LAUNCH : this.appLaunched(); break;

                case InvokeStartupMode.VIEWER : this.appViewer(); break;

                default : trace( 'other' ); break;

        }

}

 

In each of the case functions, they would perform certain functions during the startup of the application. 

 

Depending on your app, it might be a good idea to maintain the initial invocation method that was used to start your application in case that state may govern logic flow or UI layout in your application.  For example, your app as a viewer may not show certain UI components or prevent certain actions from occurring because it is embedded in another application.

 

CLIENT VIEWER

If you allow your app to be a client viewer that is embedded in the target application, or you are using an client app to be embedded in your app, please be aware that the app is in its own window that is overlaid into the target app.  You have to take a few extra measures to treat the client viewer app as a normal component in the application.  This includes:

  • Create
  • Move
  • Show
  • Hide

There is a sample class at the end of this tutorial that tries to address some of these issues.

 

Overlay parent controls

If there are other controls on the client page that might extend over the viewer, such as drop down menus and pickers, it will not display over the viewer.  This is because the viewer is a separate window that is not really a part of the display list of the application.  Your choices are to:

  • Use different drop down controls to perform similar actions
  • Hide the viewer when the control is selected
  • Move the viewer down when the control is selected (and possibly resize since there might be controls below the viewer)

Luckily, the Action Bar overflow seems to be its own window as well, and will show over your client viewer when displayed.

 

In hiding a viewer (because you are going to another page), you will experience a slight delay before the viewer actually hides.  It is probably best to pause before you actually hide the page, so that the viewer has time to hide.  Better UX.

 

 

MESSAGES

Messages can be sent to and from client and target.  This allows both applications to communicate bi-directionally to either request or inform each application on the current state.  For example, a client application could send a message to a target image application to open a file, crop it to a requested size and save it under a new name.  The target application can inform the requesting client, when that process is complete.  Another example is that a client application could request a mapping application when the user’s current position has changed by a “significant” amount.  Then the target mapping application could send a message to the requesting client when that event occurs.

 

Messages are ByteArrays.  The content of the data is largely dependent on the target application.  Data could be a simple reference to a file, or it could be the contents of the file.  The data could be an ordered sequence of data.  For example:

 

var data : ByteArray = new ByteArray();

data.writeUTF( ‘address’ );

data.writeUTF( ‘Washington, DC’ );

 

 

Or the data could be a sequence of name/value pairs, so that the order would be less important (good for later growth to the interface).

 

 

var data : ByteArray = new ByteArray();

data.writeUTF( ‘address=Washington, DC’ );

 

 

Or the data could be an object contained the name/value pairs:

 

 

var data : ByteArray = new ByteArray();

data.writeObject( {address:’Washington, DC’} );

 

 

Or the data could be JSON or XML:

 

 

var data : ByteArray = new ByteArray();

data.writeUTF( ‘<data><address>Washington,DC</address></data>’ );

 

 

In other words, there really is no one method to send data to a target application.  It is more important that the target application clearly publishes the interface to the application, allowing for growth and changes to the interface as new features are added over time.

 

CLIENT TO TARGET

A client can send an initial message to a target upon the initial invocation.  The target application will receive this when the initial invocation event is received.

 

 

var data : ByteArray = new ByteArray();

data.writeUTC( ‘address’ );

data.writeUTC( ‘Washington, DC’ );

 

var request :InvokeViewerRequest = new InvokeViewerRequest();

request.action   = InvokeAction.OPEN;

request.target   = ‘com.company.app.viewer’;

request.windowId = ‘myid’;

request.windowHeight = 600;

request.windowWidth  = 800;

request.data         = data;

var viewer :Viewer = InvokeManager.invokeManager.invokeViewer( request );

 

 

The target will get the invocation request as discussed earlier, and then the target can parse the data to determine the type of request being made.

 

 

var request :InvokeViewerRequest = InvokeManager.invokeManager.startupViewerRequest;

if( request.data != null )

{

  var type : String = data.readUTF();

  if( type == ‘address’ )

  {

    this.processAddress( data.readUTF() );

  }

}

 

 

This method is wholly dependent on how the target is to expect to receive the data.

 

Once a target has been invoked, the client can still send messages to the target.  This will be added for the Gold release of the SDK and details here will be updates after that release.

 

TBD (the API will be available in the Gold release)

 

 

The target application needs to add the following listeners to properly receive these requests:

 

TBD (the API will be available in the Gold release)

 

 

 

TARGET TO CLIENT

A target can send a message to the client application that originated the invocation of the application.   This can be done by calling:

 

InvokeManager.invokeManager.viewerSendMessage( ‘filedone’, { path : file.nativePath } );

 

 

In this case, a message of type “filedone” is sent to the client application with some associated data (path of the file).

 

The calling client will need to listen for incoming messages by listening for an event on the Viewer:

 

this.viewer.addEventListener( ViewerEvent.VIEWER_MESSAGE, MessageReceived );

 

 

And the event listener would process the incoming message from the target:

 

private function MessageReceived( event : ViewerEvent ) : void

{

  switch( event.message )

  {

    case ‘filedone’ : /* do something */ break;

    default : break;

  }

 

}

 

QUERY

An application can query what targets are available based on the mime type and the requested action to be made.  This is used to perform a bounded request but first attempting to find what targets are available so that the user can choose the best target for their needs.  This information can be displayed as a preference setting DropDown menu or as a context action upon selection.

 

To perform the query, the application can do something like this:

/////////////////////////////////////////////////////////////////////

private function Query( event : Event ) : void

{

        InvokeManager.invokeManager.addEventListener( InvokeQueryTargetEvent.SUCCESS, QueryReply );

        InvokeManager.invokeManager.queryInvokeTargets( 'application/x-map', '', InvokeAction.OPEN,

                                                        InvokeTargetOptions.APPLICATION );

}

In this example, the application is asking what mapping applications are available to open as an application (not as a viewer).

 

NOTE: The mime-type of ‘application-x-map’ may or may not be the official BlackBerry mime type for mapping applications.

 

The event listener would them be:

/////////////////////////////////////////////////////////////////////

private function QueryReply( event : InvokeQueryTargetEvent ) : void

{

        InvokeManager.invokeManager.removeEventListener( InvokeQueryTargetEvent.SUCCESS, QueryReply );

                       

        var action :ActionQuery;

        var target :InvokeTarget;

        for each( action in event.actions )

        {

                trace( 'query ' + action.label + ' action' + action.action + ' ' + action.defaultTarget );

                for each( target in action.targets )

                {

                        trace( 'target [' + target.label + '] icon=[' + target.icon + ']' );

                        this.image.setImage( 'file://' + target.icon );

                }

        }

}

 

You will also see in this example that an image is being set to the icon URI returned from the target.  Please note that this is a string and the image is prepending this with ‘file://’ to properly load the image. The actual items would be normally loaded in a DropDown or a List with custom renderers.

 

So the label would be: action.label + ‘ ‘ + target.label;

The icon would be : ‘file://’ + target.icon;

The invocation target id would be: action.defaultTarget;

 

InvokeViewer Class

In dealing with the viewer can take a little effort since it does not behave like a normal UIComponent.  You can encapsulate a Viewer instance inside a UIComponent to make things a little easier.  Here is a sample class that I use in dealing with a Viewer.  If you have ideas to improve this class, please drop me a line.

package com.lib.playbook.views

{

        import flash.events.Event;

        import flash.utils.ByteArray;

       

        import mx.effects.Move;

       

        import qnx.display.Viewer;

        import qnx.events.ViewerEvent;

        import qnx.fuse.ui.core.UIComponent;

        import qnx.fuse.ui.progress.ActivityIndicator;

        import qnx.invoke.InvokeManager;

        import qnx.invoke.InvokeViewerRequest;

       

        public class InvokedViewer extends UIComponent

        {

                private var viewer   : Viewer = null;

                private var activity : ActivityIndicator = new ActivityIndicator();

                public var isReady : Boolean = false;

               

                ///////////////////////////////////////////////////////////////////////

                public function InvokedViewer()

                {

                        super();

                       

                        this.activity.visible = false;

                        this.addChild( this.activity );

                }

               

                //////////////////////////////////////////////////////////////////////

                public function dispose() : void

                {

                  if( this.viewer )

                  {

                          this.viewer.removeEventListener(ViewerEvent.VIEWER_MESSAGE, MessageReceived );

                          this.viewer.removeEventListener(ViewerEvent.VIEWER_CLOSING, Closing );

                          this.viewer.removeEventListener(ViewerEvent.VIEWER_CLOSED, Closed );

                          this.viewer.removeEventListener(ViewerEvent.VIEWER_RESIZE, Resized );

                         

                          this.removeEventListener( Event.ADDED_TO_STAGE, AddedToStage );

                          this.removeEventListener( Event.REMOVED_FROM_STAGE, RemovedFromStage );

                          this.removeEventListener( Event.LOCATION_CHANGE, Moved );

                         

                          this.viewer.dispose();

                          this.viewer = null;

                          this.isReady = false;

                  }

                }

               

                                        /////////////////////////////////////////////////////////////////////////////////////////////

        public function requestTarget( id: String, target : String, action : String, data : ByteArray ) : void

        {

                this.id = id;

                trace( 'id ' + this.id );

                       

                        this.activity.visible = true;

                        this.activity.animate( true );

                       

                        //

                        var request :InvokeViewerRequest = new InvokeViewerRequest();

                        request.action   = action;

                        request.target   = target;

                        request.windowId = id;

                        request.windowHeight = this.height;

                        request.windowWidth  = this.width;

                        request.data         = data;

                       

                        this.viewer = InvokeManager.invokeManager.invokeViewer( request );

                        this.viewer.x = this.x;

                        this.viewer.y = this.y;

                        //this.map_card.resize( 600, 1000 );

                        this.viewer.zOrder = -1;

                        this.viewer.visible = true;

                        this.viewer.addEventListener(ViewerEvent.VIEWER_CREATED, ViewerCreated );

                        this.viewer.addEventListener(ViewerEvent.VIEWER_MESSAGE, MessageReceived );

                        this.viewer.addEventListener(ViewerEvent.VIEWER_CLOSING, Closing );

                        this.viewer.addEventListener(ViewerEvent.VIEWER_CLOSED, Closed );

                        this.viewer.addEventListener(ViewerEvent.VIEWER_RESIZE, Resized );

                       

                        //trace( 'x ' + this.x + ' y ' + this.y );

                       

                        this.addEventListener( Event.ADDED_TO_STAGE, AddedToStage );

                        this.addEventListener( Event.REMOVED_FROM_STAGE, RemovedFromStage );

                        this.addEventListener( Event.LOCATION_CHANGE, Moved );

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                private function ViewerCreated( event : ViewerEvent ) : void

                {

                        this.viewer.removeEventListener(ViewerEvent.VIEWER_CREATED, ViewerCreated );

                       

                        this.activity.visible = false;

                        this.activity.animate( false );

 

                        this.viewer.x = this.x;

                        this.viewer.y = this.y;

                       

                        this.isReady = true;

                        this.dispatchEvent( new ViewerEvent( ViewerEvent.VIEWER_CREATED ) );

                }

               

                ///////////////////////////////////////////////////////////////////////////////

                override public function setPosition(x:Number, y:Number):void

                {

                        super.setPosition( x, y );

                        if( this.viewer )

                        {

                                this.viewer.x = x;

                                this.viewer.y = y;

                        }

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                public function sendMessage( msg : String ) : void

                {

                        InvokeManager.invokeManager.viewerSendMessage( "message", {id:'bob'} );

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                private function MessageReceived( event : ViewerEvent ) : void

                {

                        trace( 'message received [' + event.message + ']' );

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                private function Closing( event : ViewerEvent ) : void

                {

                        trace( 'Closing' );

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                private function Closed( event : ViewerEvent ) : void

                {

                        trace( 'Closed' );

                }

               

                /////////////////////////////////////////////////////////////////////////////////

                private function Resized( event : ViewerEvent ) : void

                {

                        trace( 'Resized' );

                }

               

                ////////////////////////////////////////////////////////////////////////////////

                override public function set visible( value : Boolean ) : void

                {

                        super.visible = value;

                        if( this.viewer )

                        {

                                this.viewer.zOrder = ( value ? 0 : -1 );

                                this.viewer.visible = value;

                                trace( 'visible ' + value );

                        }

                }

               

                /////////////////////////////////////////////////////////////////////////////////////

                private function AddedToStage( event :Event ) : void

                {

                        if( this.viewer != null )

                        {

                                this.viewer.zOrder = 0;

                            this.viewer.visible = true;

                        }

                }

               

                /////////////////////////////////////////////////////////////////////////////////////

                private function Moved( event :Event ) : void

                {

                        trace( 'moved' );

                }

               

                /////////////////////////////////////////////////////////////////////////////////////

                private function RemovedFromStage( event :Event ) : void

                {

                        if( this.viewer )

                        {

                          this.viewer.visible = false;

                        }

                }

               

                /////////////////////////////////////////////////////////////////////////////////////

                override protected function updateDisplayList(unscaledWidth:Number,

                                                                unscaledHeight:Number):void

                {

                        this.activity.setPosition( ( unscaledWidth - this.activity.width ) / 2,

                                                ( unscaledHeight - this.activity.height ) / 2 );

                       

                        if( this.viewer )

                        {

                                this.viewer.resize( unscaledWidth, unscaledHeight );

                        }

                }

        }

}