| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package mx.managers |
| { |
| |
| import flash.display.DisplayObject; |
| import flash.display.LoaderInfo; |
| import flash.events.StatusEvent; |
| import flash.events.TimerEvent; |
| import flash.net.navigateToURL; |
| import flash.net.URLRequest; |
| import flash.utils.Dictionary; |
| import flash.utils.Timer; |
| import mx.core.FlexGlobals; |
| import mx.core.mx_internal; |
| import mx.core.Singleton; |
| import mx.events.BrowserChangeEvent; |
| import mx.managers.BrowserManager; |
| import mx.managers.IHistoryManagerClient; |
| import mx.managers.HistoryManagerGlobals; |
| |
| use namespace mx_internal; |
| |
| [ExcludeClass] |
| |
| /** |
| * @private |
| */ |
| public class HistoryManagerImpl implements IHistoryManager |
| { |
| include "../core/Version.as"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Implementation notes |
| // |
| //-------------------------------------------------------------------------- |
| |
| /* |
| HistoryManager works in conjunction with certain components |
| and with an invisible "history SWF". |
| |
| The history SWF |
| --------------- |
| |
| The HTML wrapper for an MXML application contains an invisible iframe |
| named _history which contains history.swf. |
| |
| This SWF's sole purpose is to encode state information for the |
| MXML application in the query parameters of its URL. |
| |
| HistoryManager-aware components |
| ------------------------------- |
| |
| In order to support history management, a component must implement |
| the IHistoryManagerClient interface and must register with HistoryManager. |
| |
| When the application state is considered to have changed, someone |
| must call <code>HistoryManager.save()</code>; in response, the HistoryManager calls |
| the <code>saveState()</code> method of all the registered components. |
| |
| Each registered component returns an Object containing arbitrary |
| state information such as { selectedIndex: 1 }. |
| |
| HistoryManager encodes the state information for all registered |
| components into the query parameters of a URL for the history SWF. |
| For example, the query parameters might be |
| |
| ?ce41-selectedIndex=1&10f7-selectedIndex=2 |
| |
| In order to keep the length of the URL within the browser's limits, |
| the identity of the component is specified by a short hash string |
| (such as "ce41" or "1047" above) computed from its toString() string. |
| |
| The <code>save()</code> method reloads the _history iframe with the new URL |
| that contains the MXML apps' state info in its query parameters. |
| |
| The browser stores these various URLs, representing various states |
| of the application, in its history cache. |
| |
| When the user clicks the browser's Back button, the browser loads |
| a previous URL into the _history iframe. This causes the history.swf |
| to be reloaded. |
| |
| When the history.swf is reloaded, it decodes its URL and calls |
| HistoryManager.load(). HistoryManager then calls the loadState() |
| methods of all registered components. It ensures that loadState() |
| is called on shallowly nested components before being called |
| on more deeply nested components, so that, for example, an Accordion |
| inside a ViewStack works properly. |
| |
| Handshakes |
| ---------- |
| |
| HistoryManager is initialized when the application starts. |
| |
| When HistoryManager is initialized by the init() method, |
| it periodically attempts to contact the history.swf |
| using a LocalConnection to determine whether it has been loaded |
| and has completed its own initialization. |
| |
| The "handshake" consists of the following sequence: |
| HistoryManager.initHandshake() |
| register() in history.swf |
| HistoryManager.registered() |
| |
| A corresponding handshake is initiated by the initApp() method |
| of the history.swf. |
| |
| It consists of the following sequence: |
| initHandshake() in history.swf |
| HistoryManager.registerHandshake() |
| registered() in history.swf |
| HistoryManager.load() |
| |
| */ |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Class constants |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| * The interval between handshake attempts. |
| */ |
| private static const HANDSHAKE_INTERVAL:int = 500; // milliseconds |
| |
| /** |
| * @private |
| * The maximum number of handshake attempts. |
| */ |
| private static const MAX_HANDSHAKE_TRIES:int = 100; |
| |
| /* |
| * The following three constants are used to encode state information |
| * about multiple objects into the query parameters of an URL. |
| * The query parameters might look like this: |
| * ?8ce2-selectedIndex=1&47f0-selectedIndex=2 |
| */ |
| |
| /** |
| * @private |
| * The separator between the object identifier (the crc of its path) |
| * and the property name. |
| */ |
| private static const ID_NAME_SEPARATOR:String = "-"; |
| |
| /** |
| * @private |
| * The separator between the property name and the property value. |
| */ |
| private static const NAME_VALUE_SEPARATOR:String = "="; |
| |
| /** |
| * @private |
| * The separator between consecutive object/property/value triples. |
| */ |
| private static const PROPERTY_SEPARATOR:String = "&"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Class variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| private static var systemManager:ISystemManager; |
| |
| /** |
| * @private |
| */ |
| private static var instance:IHistoryManager; |
| |
| /** |
| * @private |
| * CRC for the app. |
| */ |
| private static var appID:String; |
| |
| /** |
| * @private |
| * URL for history frame. |
| * This is passed to the SWF via the FlashStatic Vars in the HTML wrapper, |
| * and is currently "/dev/flex-internal?action=history_html". |
| */ |
| private static var historyURL:String; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Class methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| public static function getInstance():IHistoryManager |
| { |
| if (!instance) |
| { |
| systemManager = SystemManagerGlobals.topLevelSystemManagers[0]; |
| instance = new HistoryManagerImpl(); |
| } |
| |
| return instance; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| public function HistoryManagerImpl() |
| { |
| super(); |
| |
| if (instance) |
| throw new Error("Instance already exists."); |
| |
| if (appID) |
| return; |
| |
| if (!("historyManagementEnabled" in FlexGlobals.topLevelApplication) || |
| !FlexGlobals.topLevelApplication.historyManagementEnabled) |
| return; |
| |
| var loaderInfo:LoaderInfo; |
| |
| // Added to support Cross-versioning issue, |
| // so one needs to set the Singleton.loaderInfo object |
| // to the loaderInfo of the top-most object |
| if (HistoryManagerGlobals.loaderInfo) |
| { |
| loaderInfo = HistoryManagerGlobals.loaderInfo; |
| } |
| else |
| { |
| // Get values that were passed in via the FlashVars |
| // in the HTML wrapper. |
| loaderInfo = DisplayObject(systemManager).loaderInfo; |
| } |
| |
| var appURL:String; |
| |
| // Use our URL as the unique CRC for this movie. |
| if (HistoryManagerGlobals.loaderInfo) |
| appURL = HistoryManagerGlobals.loaderInfo.url; |
| else |
| appURL = DisplayObject(systemManager).loaderInfo.url; |
| |
| |
| appID = calcCRC(appURL); |
| |
| BrowserManager.getInstance().addEventListener(BrowserChangeEvent.BROWSER_URL_CHANGE, browserURLChangeHandler); |
| BrowserManager.getInstance().initForHistoryManager(); |
| |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @Private |
| * An Array of objects that will save and load state information. |
| * Each object must implement the IHistoryManagerClient interface. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| private var registeredObjects:Array = []; |
| |
| /** |
| * @private |
| * A map from a registed object's path to its RegistrationInfo |
| * (path crc and path depth). |
| */ |
| private var registrationMap:Dictionary; |
| |
| /** |
| * @private |
| * Pending states for deferred restoration. |
| */ |
| private var pendingStates:Object = {}; |
| |
| /** |
| * @private |
| * Pending query string to be sent to history.swf. |
| */ |
| private var pendingQueryString:String; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Registers an object with the HistoryManager. |
| * The object must implement the IHistoryManagerClient interface. |
| * |
| * @param obj Object to register. |
| * |
| * @see mx.managers.IHistoryManagerClient |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function register(obj:IHistoryManagerClient):void |
| { |
| if (!("historyManagementEnabled" in FlexGlobals.topLevelApplication) || |
| !FlexGlobals.topLevelApplication.historyManagementEnabled) |
| return; |
| |
| // Ensure that this object isn't already registered. |
| unregister(obj); |
| |
| // Add the object to the Array of all registered objects. |
| registeredObjects.push(obj); |
| |
| // Get a "path" string such as "Application_1.VBox0.hb:HBox.main: |
| // Panel.bodyStack:ViewStack.checkoutView:VBox.accordion:Accordion" |
| // that uniquely represents this object in the visual hierarchy |
| // of the application. |
| var path:String = getPath(obj); |
| |
| // Calculate a 4-character hex CRC of this path as a short |
| // identifier for the object to be encoded into the |
| // query parameters of the history URL. |
| var crc:String = calcCRC(path); |
| |
| // Determine the depth of the object in the visual hierarchy. |
| // This is important because state must be restored |
| var depth:int = calcDepth(path); |
| |
| // Store the crc and depth of the registered object in a |
| // RegistrationInfo instance kept in the HistoryManager's |
| // registrationMap. Given a registed object, we can look up |
| // its crc and depth using getRegistrationInfo(). |
| if (!registrationMap) |
| registrationMap = new Dictionary(true); |
| registrationMap[obj] = new RegistrationInfo(crc, depth); |
| |
| // Sort the Array of all registered objects according to their depth. |
| registeredObjects.sort(depthCompare); |
| |
| // See if there is a pending state for this object. |
| if (pendingStates[crc]) |
| { |
| obj.loadState(pendingStates[crc]); |
| delete pendingStates[crc]; |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| private function getPath(obj:IHistoryManagerClient):String |
| { |
| return obj.toString(); |
| } |
| |
| /** |
| * @private |
| * Function to calculate a cyclic rendundancy checksum (CRC). |
| * This returns a 4-character hex string representing a 16-bit uint |
| * calculated from the specified string using the CRC-CCITT mask. |
| * In http://www.joegeluso.com/software/articles/ccitt.htm, |
| * the following sample input and output is given to check |
| * this implementation: |
| * "" -> "1D0F" |
| * "A" -> "9479" |
| * "123456789" -> "E5CC" |
| * "AA...A" (256 A's) ->"E938" |
| */ |
| private function calcCRC(s:String):String |
| { |
| var crc:uint = 0xFFFF; |
| |
| // Process each character in the string. |
| var n:int = s.length; |
| for (var i:int = 0; i < n; i++) |
| { |
| var charCode:uint = s.charCodeAt(i); |
| |
| // Unicode characters can be greater than 255. |
| // If so, we let both bytes contribute to the CRC. |
| // If not, we let only the low byte contribute. |
| var loByte:uint = charCode & 0x00FF; |
| var hiByte:uint = charCode >> 8; |
| if (hiByte != 0) |
| crc = updateCRC(crc, hiByte); |
| crc = updateCRC(crc, loByte); |
| } |
| |
| // Process 2 additional zero bytes, as specified by the CCITT algorithm. |
| crc = updateCRC(crc, 0); |
| crc = updateCRC(crc, 0); |
| |
| return crc.toString(16); |
| } |
| |
| /** |
| * @private |
| */ |
| private function updateCRC(crc:uint, byte:uint):uint |
| { |
| const poly:uint = 0x1021; // CRC-CCITT mask |
| |
| var bitMask:uint = 0x80; |
| |
| // Process each bit in the byte. |
| for (var i:int = 0; i < 8; i++) |
| { |
| var xorFlag:Boolean = (crc & 0x8000) != 0; |
| |
| crc <<= 1; |
| crc &= 0xFFFF; |
| |
| if ((byte & bitMask) != 0) |
| crc++; |
| |
| if (xorFlag) |
| crc ^= poly; |
| |
| bitMask >>= 1; |
| } |
| |
| return crc; |
| } |
| |
| /** |
| * @private |
| */ |
| private function calcDepth(path:String):int |
| { |
| return path.split(".").length; |
| } |
| |
| /** |
| * @private |
| */ |
| private function depthCompare(a:Object, b:Object):int |
| { |
| var regInfoA:RegistrationInfo = getRegistrationInfo(IHistoryManagerClient(a)); |
| var regInfoB:RegistrationInfo = getRegistrationInfo(IHistoryManagerClient(b)); |
| |
| // Guard against the possibility of an object's |
| // registration info not being found. |
| if (!regInfoA || !regInfoB) |
| return 0; |
| |
| if (regInfoA.depth > regInfoB.depth) |
| return 1; |
| |
| if (regInfoA.depth < regInfoB.depth) |
| return -1; |
| |
| return 0; |
| } |
| |
| /** |
| * @private |
| */ |
| private function getRegistrationInfo( |
| obj:IHistoryManagerClient):RegistrationInfo |
| { |
| return registrationMap ? registrationMap[obj] : null; |
| } |
| |
| /** |
| * Unregisters an object with the HistoryManager. |
| * |
| * @param obj Object to unregister. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function unregister(obj:IHistoryManagerClient):void |
| { |
| if (!("historyManagementEnabled" in FlexGlobals.topLevelApplication) || |
| !FlexGlobals.topLevelApplication.historyManagementEnabled) |
| return; |
| |
| // Find the index of the object in the Array of all |
| // registered objects; -1 means not found. |
| var index:int = -1; |
| var n:int = registeredObjects.length; |
| for (var i:int = 0; i < n; i++) |
| { |
| if (registeredObjects[i] == obj) |
| { |
| index = i; |
| break; |
| } |
| } |
| |
| // If the object was found in the Array, remove it. |
| if (index >= 0) |
| registeredObjects.splice(index, 1); |
| |
| // Remove it from the map as well. |
| if (obj && registrationMap) |
| delete registrationMap[obj]; |
| } |
| |
| /** |
| * Saves the application's current state so it can be restored later. |
| * This method is automatically called by navigator containers |
| * whenever their navigation state changes. |
| * If you registered an interface with the HistoryManager, |
| * you are responsible for calling the <code>save()</code> method |
| * when the application state changes. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function save():void |
| { |
| if (!("historyManagementEnabled" in FlexGlobals.topLevelApplication) || |
| !FlexGlobals.topLevelApplication.historyManagementEnabled) |
| return; |
| |
| var haveState:Boolean = false; |
| |
| // Query string always starts with the application identifier. |
| var queryString:String = "app=" + appID; |
| |
| // Call saveState() on every registered object |
| // to get an Object containing its state information. |
| var n:int = registeredObjects.length; |
| for (var i:int = 0; i < n; i++) |
| { |
| var registeredObject:IHistoryManagerClient = registeredObjects[i]; |
| var stateInfo:Object = registeredObject.saveState(); |
| |
| // stateInfo might be something like { selectedIndex: 1 } |
| |
| // Encode the stateInfo into the query string, building up |
| // a string like "ce41-selectedIndex=1&10f7-selectedIndex=2" |
| // that specifies objects (via the crcs of their paths), |
| // property names, and property values. |
| var crc:String = getRegistrationInfo(registeredObject).crc; |
| for (var name:String in stateInfo) |
| { |
| var value:Object = stateInfo[name]; |
| |
| if (queryString.length > 0) |
| queryString += PROPERTY_SEPARATOR; |
| queryString += crc; |
| queryString += ID_NAME_SEPARATOR; |
| queryString += escape(name); |
| queryString += NAME_VALUE_SEPARATOR; |
| queryString += escape(value.toString()); |
| haveState = true; |
| } |
| } |
| |
| // If any registered objects specified any state information to save, |
| // reload the history SWF with an URL that encodes all the state info. |
| if (haveState) |
| { |
| pendingQueryString = queryString; |
| FlexGlobals.topLevelApplication.callLater(this.submitQuery); |
| } |
| } |
| |
| /** |
| * @private |
| * Reloads the _history iframe with the history SWF. |
| */ |
| private function submitQuery():void |
| { |
| if (pendingQueryString) |
| { |
| BrowserManager.getInstance().setFragment(pendingQueryString); |
| pendingQueryString = null; |
| if ("resetHistory" in FlexGlobals.topLevelApplication) |
| FlexGlobals.topLevelApplication.resetHistory = true; |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Event handlers |
| // |
| //-------------------------------------------------------------------------- |
| |
| |
| /** |
| * @private |
| * Loads state information. |
| * Called by the registered() method of history.swf |
| * after we have returned its handshake. |
| * |
| * @param stateVars State information. |
| */ |
| public function browserURLChangeHandler(event:BrowserChangeEvent):void |
| { |
| var p:String; |
| var crc:String; |
| |
| if (!("historyManagementEnabled" in FlexGlobals.topLevelApplication) || |
| !FlexGlobals.topLevelApplication.historyManagementEnabled) |
| return; |
| |
| var pieces:Array = event.url.split(PROPERTY_SEPARATOR); |
| var stateVars:Object = {}; |
| var n:int = pieces.length; |
| for (var i:int = 0; i < n; i++) |
| { |
| var nameValuePair:Array = pieces[i].split(NAME_VALUE_SEPARATOR); |
| stateVars[nameValuePair[0]] = parseString(nameValuePair[1]); |
| } |
| |
| var params:Object = {}; |
| |
| |
| // Unpack the stateVars into parameter objects for each state interface. |
| // stateVars looks like |
| // { ce41-selectedIndex: 1, 10f7-selectedIndex: 2 } |
| // params will look like |
| // { ce41: { selectedIndex: 1 }, 10f7: { selectedIndex: 2 } } |
| for (p in stateVars) |
| { |
| var crclen:int = p.indexOf(ID_NAME_SEPARATOR) |
| if (crclen > -1) |
| { |
| crc = p.substr(0, crclen); |
| var name:String = p.substr(crclen + 1, p.length); |
| var value:Object = stateVars[p]; |
| |
| if (!params[crc]) |
| params[crc] = {}; |
| |
| params[crc][name] = value; |
| } |
| } |
| |
| // Call loadState() on all registered objects. |
| n = registeredObjects.length; |
| for (i = 0; i < n; i++) |
| { |
| var registeredObject:IHistoryManagerClient = registeredObjects[i]; |
| crc = getRegistrationInfo(registeredObject).crc; |
| |
| registeredObject.loadState(params[crc]); |
| delete params[crc]; |
| } |
| |
| // Save off any remaining state variables in the pendingStates object. |
| // This way the state can be restored if the interface |
| // is instantiated later. |
| for (p in params) |
| { |
| pendingStates[p] = params[p]; |
| } |
| |
| } |
| |
| private function parseString(s:String):Object |
| { |
| if (s == "true") |
| return true; |
| if (s == "false") |
| return false; |
| |
| var i:int = parseInt(s); |
| if (i.toString() == s) |
| return i; |
| |
| var n:Number = parseFloat(s); |
| if (n.toString() == s) |
| return n; |
| |
| return s; |
| } |
| } |
| |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Helper class: RegistrationInfo |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * @private |
| */ |
| class RegistrationInfo |
| { |
| include "../core/Version.as"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Constructor. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public function RegistrationInfo(crc:String, depth:int) |
| { |
| super(); |
| |
| this.crc = crc; |
| this.depth = depth; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| public var crc:String; |
| |
| /** |
| * @private |
| */ |
| public var depth:int; |
| } |