We’ve pushed the simple model of single application stage far enough to incorporate services, notifications, and even dashboard stages, but we need to move to a more advanced model to access the rest of the features. An advanced application will have some or all of these characteristics:
Use an application assistant as the main application entry point and for handling application initialization and coordination.
Create a primary card stage when launched.
Handle relaunch or remote launch requests through a defined
handleLaunch
method.
Post banner notifications and maintain a dashboard panel for events while not in focus or in the background.
Schedule wakeup requests through the Alarm service and handle the alarm callbacks in the background.
If you aren’t clear on the application lifecycle or the role of the application assistant, you may want to review Chapter 2 before reading the rest of this chapter.
This chapter began with a list of guidelines for developing multistage applications. News needs to be cleaned up to conform to those guidelines, so before creating the app assistant, we’ll make these changes to News:
Remove use of the global window object; change window.setInterval()
to this.controller.window.setInterval()
.
Use the local controller’s stageController
methods; instead of
Mojo.Controller.stageController
methods for pushScene
, we’ll use
swapScene
, as in this example in
storyView-assistant.js in the
handleCommand
method:
case "do-viewNext": Mojo.Controller.stageController.swapScene( { transition: Mojo.Transition.crossFade, name: "storyView" }, this.storyFeed, this.storyIndex+1); break;
Add the noWindow
property
to appinfo.json:
{ "title": "News", "type": "web", "main": "index.html", "id": "com.palm.app.news", "version": "1.0.0", "vendor": "Palm", "noWindow" : "true", "icon": "icon.png", "theme": "light" }
And add the app assistant to sources.json; remember that it must be the first entry:
[ { "source": "app/assistants/app-assistant.js" }, { "source": "app/assistants/stage-assistant.js" }, . . . ]
We’ll create a minimal application assistant first, and then flesh
it out step by step so that you can see each part clearly. Initially,
we’ll simply move all the code from the stage assistant to app-assistant.js, removing the call to
this.controller.pushScene()
and
adding the handleLaunch
method to
create the card stage and push the first scene:
/* AppAssistant - NEWS Copyright 2009 Palm, Inc. All rights reserved. Responsible for app startup, handling launch points and updating news feeds. Major components: - setup; app startup including preferences, initial load of feed data from the Depot and setting alarms for periodic feed updates - handleLaunch; launch entry point for initial launch, feed update alarm, dashboard or banner tap - handleCommand; handles app menu selections Data structures: - globals; set of persistant data used throughout app - Feeds Model; handles all feedlist updates, db handling and default data - Cookies Model; handles saving and restoring preferences App architecture: - AppAssistant; handles startup, feed list management and app menu management - FeedListAssistant; handles feedList navigation, search, feature feed - StoryListAssistant; handles single feed navigation - StoryViewAssistant; handles single story navigation - PreferencesAssistant; handles preferences display and changes - DashboardAssistant; displays latest new story and new story count */ // --------------------------------------------------------------- // GLOBALS // --------------------------------------------------------------- // News namespace News = {}; // Constants News.unreadStory = "unReadStyle"; News.versionString = "1.0"; News.MainStageName = "newsStage"; News.DashboardStageName = "newsDashboard"; News.errorNone = "0"; // No error, success News.invalidFeedError = "1"; // Not RSS2, RDF (RSS1), or ATOM // Global Data Structures // Persistent Globals - will be saved across app launches News.featureFeedEnable = false; // Enables feed rotation News.featureStoryInterval = 5000; // Feature Interval (in ms) News.notificationEnable = true; // Enables notifcations News.feedUpdateBackgroundEnable = false; // Enable device wakeup News.feedUpdateInterval = 900000; // Feed update interval // Session Globals - not saved across app launches News.feedListChanged = false; // Triggers update to Depot db News.feedListUpdateInProgress = false; // Feed update is in progress News.featureStoryTimer = null; // Timer for story rotations News.dbUpdate = ""; // Default is no update News.wakeupTaskId = undefined; // Id for wakeup tasks // Setup App Menu for all scenes; all menu actions handled in // AppAssistant.handleCommand() News.MenuAttr = {omitDefaultItems: true}; News.MenuModel = { visible: true, items: [ {label: "About News...", command: "do-aboutNews"}, Mojo.Menu.editItem, {label: "Update All Feeds", checkEnabled: true, command: "do-feedUpdate"}, {label: "Preferences...", command: "do-newsPrefs"}, Mojo.Menu.helpItem ] }; function AppAssistant (appController) { } // ------------------------------------------------------- // setup - all startup actions: // - Setup globals with preferences // - Set up application menu; used in every scene // - Open Depot and use contents for feedList // - Initiate alarm for first feed update AppAssistant.prototype.setup = function() { // initialize the feeds model this.feeds = new Feeds(); this.feeds.loadFeedDb(); // load preferences and globals from saved cookie News.Cookie.initialize(); // Set up first timeout alarm this.setWakeup(); }; // ------------------------------------------------------- // handleLaunch - called by the framework when the application is asked to launch // - First launch; create card stage and first first scene // - Update; after alarm fires to update feeds // - Notification; after user taps banner or dashboard // AppAssistant.prototype.handleLaunch = function (launchParams) { Mojo.Log.info("ReLaunch"); var cardStageController = this.controller.getStageController(News.MainStageName); var appController = Mojo.Controller.getAppController(); if (!launchParams) { // FIRST LAUNCH // Look for an existing main stage by name. if (cardStageController) { // If it exists, just bring it to the front by focusing its window. Mojo.Log.info("Main Stage Exists"); cardStageController.popScenesTo("feedList"); cardStageController.activate(); } else { // Create a callback function to set up the new main stage // once it is done loading. It is passed the new stage controller // as the first parameter. var pushMainScene = function(stageController) { stageController.pushScene("feedList", this.feeds); }; Mojo.Log.info("Create Main Stage"); var stageArguments = {name: News.MainStageName, lightweight: true}; this.controller.createStageWithCallback(stageArguments, pushMainScene.bind(this), "card"); } } }; // ----------------------------------------- // handleCommand - called to handle app menu selections // . . . };
The framework calls the application assistant’s handleLaunch
method
after the setup
method on initial
launch, and whenever a launch request is made to the application. If you
don’t define one, the framework attaches a default handleLaunch
method, which calls your
application assistant’s setup
method.
Launch requests are made implicitly through the following:
Taps to your application’s banner notifications.
Calls from other applications through an Application Manager service request.
Alarms that wake up the application after a timeout.
By convention, you should also use this entry point for your own
launch requests. For example, instead of using the Application Manager
service request, launch your application after a tap to the Dashboard
stage by calling the entry point directly from within the handleTap
method in the Dashboard, passing an
action
property in the launchParams
object:
DashboardAssistant.prototype.launchMain = function() { Mojo.Log.info("Tap to Dashboard"); var appController = Mojo.Controller.getAppController(); appController.assistant.handleLaunch({action: "notification", index: this.index}); this.controller.window.close(); };
And we’ll add a specific case in handleLaunch
for this notification action
following the conditional set up to handle the first launch. We use a
case statement because we’ll build on this to handle other launch
actions:
// ------------------------------------------------------- // handleLaunch - called by the framework when the application is asked to launch // - First launch; create card stage and first first scene // - Update; after alarm fires to update feeds // - Notification; after user taps banner or dashboard // AppAssistant.prototype.handleLaunch = function (launchParams) { Mojo.Log.info("ReLaunch"); var cardStageController = this.controller.getStageController(News.MainStageName); var appController = Mojo.Controller.getAppController(); if (!launchParams) { // FIRST LAUNCH // Look for an existing main stage by name. if (cardStageController) { // If it exists, just bring it to the front by focusing its window. Mojo.Log.info("Main Stage Exists"); cardStageController.popScenesTo("feedList"); cardStageController.activate(); } else { // Create a callback function to set up the new main stage // once it is done loading. It is passed the new stage controller // as the first parameter. var pushMainScene = function(stageController) { stageController.pushScene("feedList", this.feeds); }; Mojo.Log.info("Create Main Stage"); var stageArguments = {name: News.MainStageName, lightweight: true}; this.controller.createStageWithCallback(stageArguments, pushMainScene.bind(this), "card"); } } else { Mojo.Log.info("com.palm.app.news -- Wakeup Call", launchParams.action); switch (launchParams.action) { // NOTIFICATION case "notification" : Mojo.Log.info("com.palm.app.news -- Notification Tap"); if (cardStageController) { // If it exists, find the appropriate story list and activate it. Mojo.Log.info("Main Stage Exists"); cardStageController.popScenesTo("feedList"); cardStageController.pushScene("storyList", this.feeds.list, launchParams.index); cardStageController.activate(); } else { // Create a callback function to set up a new main stage, // push the feedList scene and then the appropriate story list var pushMainScene2 = function(stageController) { stageController.pushScene("feedList", this.feeds); stageController.pushScene("storyList", this.feeds.list, launchParams.index); }; Mojo.Log.info("Create Main Stage"); var stageArguments2 = {name: News.MainStageName, lightweight: true}; this.controller.createStageWithCallback(stageArguments2, pushMainScene2.bind(this), "card"); } break; } } };
The notification launch case is added to activate the News main
stage with the storyList
scene pushed to the selected
feed. If the stage doesn’t exist, it will be created before pushing the
storyList
scene. This type of launch request will
launch the application if it’s not already launched.
Looking at this a little more closely, if the stage exists, the
News application’s main stage is in the card view, so the scene stack is
popped back to the feedList
scene, which is always at
the base of the scene stack for this application. The
storyList
scene is pushed with the feed that was
displayed by the dashboard summary. If the main card stage does not
exist (just the dashboard is running), the stage is created before
pushing both the feedList
and
storyList
scenes. It’s important to set up the scene
stack when launching to a scene that is normally not at the base of the
stack.
This launch action is initiated by tapping either the dashboard
summary (from the code sample shown immediately prior to this
application assistant sample) and the banner notification. If you look
at the feed.js method for updateFeedSuccess()
, you’ll see that the
launch argument’s action
property is
set to "notification"
:
appController.showBanner(bannerParams, {action: "notification", index: this.feedIndex}, this.list[this.feedIndex].url);
To facilitate communication between assistants, the Mojo framework supports an application specific
notification chain. Any application assistant can pass a notification
through the chain through a call to SendToNotificationChain()
with a single hash
parameter. The current stage and scene assistants have the opportunity
to handle these notifications by including a considerForNotification()
method.
To illustrate this, we’ll send an update notification (type: "update"
) through the chain, identifying that an update is in
progress (update : true
) or just
completed (update : false
), as well as the
index of the affected feed (feedIndex : this.feedIndex
). This is
done in several places in feeds.js,
wherever we were updating the spinnerModel
and calling this.updateListModel()
. Replace this
code:
// Change feed update indicator & update widget var spinnerModel = this.list[this.feedIndex]; spinnerModel.value = true; this.updateListModel();
With this code:
// Notify the chain that there is an update in progress Mojo.Controller.getAppController().sendToNotificationChain({ type: "update", update: true, feedIndex: this.feedIndex });
That code is used in three other places. The update
property should be set to
true
wherever you were previously setting the
spinnerModel.value
to
true
, and false
in the other
cases. You can remove the registerListModel()
, removeListModel()
, and updateListModel()
methods
in feeds.js and remove the calls to
those methods from the feedList-assistant.js activate()
and deactivate()
methods.
Scenes can receive notifications if you add a considerForNotification()
method to the scene
assistant. In the News example, we’ll add this to the feedList-assistant.js:
// --------------------------------------------------------------------- // considerForNotification - called by the framework when a notification // is issued; look for notifications of feed updates and update the // feedWgtModel to reflect changes, update the feed's spinner model FeedListAssistant.prototype.considerForNotification = function(params){ if (params && (params.type == "update")) { this.feedWgtModel.items = this.feeds.list; this.feeds.list[params.feedIndex].value = params.update; this.controller.modelChanged(this.feedWgtModel); // If stories exist in the this.featureIndexFeed, then start the rotation // if not already started // ** These next two lines are wrapped for book formatting only ** if ((this.feeds.list[this.featureIndexFeed].stories.length > 0) && (News.featureStoryTimer === null)) { var splashScreenElement = this.controller.get("splashScreen"); splashScreenElement.hide(); this.showFeatureStory(); } } return undefined; };
This method is called on any notification, but on update notifications it will set the affected feed’s spinner value to reflect whether an update is in progress or not, update the feed list, and start the feature story timer if needed.
In storyList-assistant.js, it’s used to look for changes to the displayed feed:
// considerForNotification - called when a notification is issued; if this // feed has been changed, then update it. StoryListAssistant.prototype.considerForNotification = function(params){ if (params && (params.type == "update")) { if ((params.feedIndex == this.feedIndex) && (params.update === false)) { this.storyModel.items = this.feed.stories; this.controller.modelChanged(this.storyModel); } } return undefined; };
Among the scenes, only the active scene’s considerForNotification()
method is called.
That’s followed by calls to the active stage assistant and finally the
application assistant. Since the application assistant is always the
last on the chain, it can process what remains once the other assistants
have had their chance at the notification block.
In this final example, we’ll create a secondary card stage by adding the option
to push a single feed into its own card. Add a “New Card” item to the
pop-up submenu, which displays when the user taps the unread count on a
specific feed in the list. When that item is tapped, it will push that
feed into a new card. Figure 10-4 shows the
new submenu and the card view showing the main
feedList
and the secondary card.
Secondary card stages are created like other stages. Here’s the
case statement from the popupHandler
method in feedList-assistant.js,
triggered by the New Card submenu selector:
case "feed-card": Mojo.Log.info("Popup tear off feed to new card:", popupFeed.title); var newCardStage = "newsCard"+this.popupIndex; var cardStage = this.appController.getStageController(newCardStage); var feedList = this.feeds.list; var feedIndex = this.popupIndex; if(cardStage) { Mojo.Log.info("Existing Card Stage"); cardStage.popScenesTo(); cardStage.pushScene("storyList", this.feeds.list, feedIndex); cardStage.activate(); } else { Mojo.Log.info("New Card Stage"); var pushStoryCard = function(stageController){ stageController.pushScene("storyList", feedList, feedIndex); }; this.appController.createStageWithCallback({ name: newCardStage, lightweight: true}, pushStoryCard, "card"); } break;
The stage name must be unique unless you plan to reuse the same
stage for each card; in this example, we use the feed index to form part
of the stage name to keep it unique. When reusing the stage, popScenesTo()
is used with pushScene()
and activate()
methods to maximize the stage with
the storyList
scene.
To enable this command option, add another choice to the submenu
in the feedList
assistant’s showFeed()
method:
{label: "New Card", command: "feed-card"}
When users select this option by tapping the info icon on any feed
in the feed list, a storyList
scene will be pushed
with the selected feed in its own card.
3.12.74.189