V49 Classes

V49 Classes for the Java SDK

Use these classes in place of the same-named SDK and Bananas classes to work with V49 TiVo features. (originally presented on forums)

V49IHmeProtocol.java - just the additional constants for the keyboard and video status, etc.
V49BApplicationFactory.java - report your app as V49 so you can use new features, fix spaces in AppTitle for bonjour.
V49BApplication.java - helpers for several V49 features, and fixes for some problems (some are pre 1.4.1e problems that don't need fixing if you use the newer SDK, so I made them forward compatible fixes).

V49IHmeProtocol.java

package com.blackledge.david.tivo;
 
import com.tivo.hme.interfaces.IHmeConstants;
import com.tivo.hme.sdk.IHmeProtocol;
 
/**
 * IHmeProtocol extension that supports some features beyond SDK 1.4.1
 * especially HTTP Header strings and Slide Remote key codes. Typically just use
 * {@link #KEY_OPT_ASCII_START}, {@link #KEY_OPT_ASCII_END}, and
 * {@link #KEY_OPT_ASCII_OFFSET} to get all the characters.
 * 
 */
public interface V49IHmeProtocol extends IHmeProtocol {
    /**
     * 0.49 - Slide Remote key events supported (no "supporting" sdk version)
     * (0.41 ({@link IHmeProtocol#VERSION_0_41}) supported by sdk 1.4, 0.43-0.45
     * ({@link IHmeProtocol#VERSION_0_43}-{@link IHmeProtocol#VERSION_0_45}) by
     * sdk 1.4.1e)
     */
    public static final int VERSION_0_49 = (0 << 8) | 49;
 
    /**
     * according to an error logged:
     * "rendered text exceeded max allowed dimensions of 1920x1080"
     */
    public static final int LIMIT_TEXT_RENDER_WIDTH = 1920;
    /**
     * according to an error logged:
     * "rendered text exceeded max allowed dimensions of 1920x1080"
     */
    public static final int LIMIT_TEXT_RENDER_HEIGHT = 1080;
 
    /** Media status code, resource at end of stream. */
    public static final int RSRC_STATUS_END = 11;
    /** Media status code, resource at end of available buffer. */
    public static final int RSRC_STATUS_BUFFER_OVERFLOW = 12;
 
    /**
     * Zoom key, optional, relabel of {@link #KEY_OPT_ASPECT} key, which is same
     * code as {@link #KEY_OPT_WINDOW} and {@link #KEY_OPT_PIP} (22).
     */
    public static final int KEY_OPT_ZOOM            = KEY_OPT_WINDOW;
 
    // undefined keys in SDK: 0-21, 23, 25, 28-50
 
    /**
     * On Demand key, reserved for internal use. E.g. long blue ON DEMAND button
     * on Charter remote. This is sent in a key press event just before
     * attempting to "teleport" to the service. In anything below Series 4, no
     * teleport and not transmitted over HME, apparently.
     */
    public static final int KEY_OPT_ON_DEMAND = 30;
 
    // last SDK key code defined: KEY_OPT_DVD = 55;
 
    /**
     * "A" yellow action remote button, optional (first on Premiere), may only transmit on if identified as V49.
     */
    public static final int KEY_OPT_ACTION_A = 56;
    /**
     * "B" blue action remote button, optional (first on Premiere), may only transmit on if identified as V49.
     */
    public static final int KEY_OPT_ACTION_B = 57;
    /**
     * "C" red action remote button, optional (first on Premiere), may only transmit on if identified as V49.
     */
    public static final int KEY_OPT_ACTION_C = 58;
    /**
     * "D" green action remote button, optional (first on Premiere), may only transmit on if identified as V49.
     */
    public static final int KEY_OPT_ACTION_D = 59;
 
    /**
     * Backspace (picture of a left arrow on slide remote) button code.<BR>
     * Name matches known network interface KEYBOARD command.
     */
    public static final int KEY_OPT_BACKSPACE = 65;
 
    /**
     * (TBD (backspace?) requires a higher version than 49) Back key, optional (first on Roamio remote).  Other "Back" keys in most apps: {@link #KEY_OPT_ACTION_B}, {@link #KEY_OPT_ZOOM}, and sometimes KEY_REPLAY.
     */
    public static final int KEY_OPT_BACK = KEY_OPT_BACKSPACE;
 
    // codes 60-64 and 66-69 are unknown, currently
 
    /**
     * Code returned by the RETURN key on the keyboard if it is modified by Alt
     * (or Windows or Ctrl or Shift (or possibly "sym"(bol))). Normally that key
     * sends KEY_SELECT.
     */
    public static final int KEY_OPT_META_SELECT = 70;
    /**
     * Left Shift button code. You do not need to process this to handle
     * capitalization, use KEY_OPT_ASCII_* to get the properly capitalized
     * character.
     * Name matches known network interface KEYBOARD command
     */
    public static final int KEY_OPT_LSHIFT = 71;
    /**
     * Right Shift button code. You do not need to process this to handle
     * capitalization, use KEY_OPT_ASCII_* to get the properly capitalized
     * character.
     * Name matches known network interface KEYBOARD command
     */
    public static final int KEY_OPT_RSHIFT = 72;
    /**
     * Left Alt (and Windows) key button code for keyboards - POSSIBLY the
     * "sym"(bol) key for the Slide Remote (although there is only one, on the
     * right side). You do not need to process this to handle symbols, use
     * KEY_OPT_ASCII_* to get the properly capitalized character.
     * Name matches known network interface KEYBOARD command
     */
    public static final int KEY_OPT_LMETA = 73;
    /**
     * Right Alt (and Windows) key button code for keyboards - POSSIBLY the
     * "sym"(bol) key for the Slide Remote. You do not need to process this to
     * handle symbols, use KEY_OPT_ASCII_* to get the properly capitalized
     * character.
     * Name matches known network interface KEYBOARD command
     */
    public static final int KEY_OPT_RMETA = 74;
    /**
     * Capslock button code. You do not need to process this to handle
     * capitalization, use KEY_OPT_ASCII_* to get the properly capitalized
     * character.
     * Name matches known network interface KEYBOARD command
     */
    public static final int KEY_OPT_CAPS = 75;
 
    /** known from network interface KEYBOARD command, just sends non-KBD code on a USB keyboard */
    public static final int KEY_OPT_KBDUP = KEY_UP;
    /** known from network interface KEYBOARD command, just sends non-KBD code on a USB keyboard */
    public static final int KEY_OPT_KBDDOWN = KEY_DOWN;
    /** known from network interface KEYBOARD command, just sends non-KBD code on a USB keyboard */
    public static final int KEY_OPT_KBDLEFT = KEY_LEFT;
    /** known from network interface KEYBOARD command, just sends non-KBD code on a USB keyboard */
    public static final int KEY_OPT_KBDRIGHT = KEY_RIGHT;
    /** known from network interface KEYBOARD command, just sends SELECT on a USB keyboard */
    public static final int KEY_OPT_KBDENTER = KEY_SELECT;
    /** known from network interface KEYBOARD command, just sends CLEAR on a USB keyboard */
    public static final int KEY_OPT_ESCAPE = KEY_CLEAR;
 
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_LCONTROL = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_RCONTROL = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_PAGEUP = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_PAGEDOWN = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_HOME = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_INSERT = KEY_UNKNOWN;
    /** NO keycode sent in V49 HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_DELETE = KEY_UNKNOWN;
 
    /** UNKNOWN keycode (same as ADVANCE?) - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_DELIMITER = KEY_ADVANCE;
 
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_CC_ON = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_CC_OFF = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_ASPECT_CORRECTION_FULL = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_ASPECT_CORRECTION_PANEL = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_ASPECT_CORRECTION_ZOOM = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_ASPECT_CORRECTION_WIDE_ZOOM = KEY_UNKNOWN;
    /** UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_VIDEO_MODE_FIXED_480i = KEY_UNKNOWN;
    /** (teleport key) UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_STANDBY = KEY_UNKNOWN;
    /** (teleport key) reported to match TIVO key... unless it matches {@link #KEY_OPT_ON_DEMAND} - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_DIRECTV = KEY_TIVO;
    /** (teleport key) UNKNOWN keycode - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_NOWSHOWING = KEY_UNKNOWN;
    /** (teleport key) UNKNOWN keycode (same as NOWSHOWING?) - may not be sent through HME, known from network interface KEYBOARD command */
    public static final int KEY_OPT_LIST = KEY_OPT_NOWSHOWING;
 
    /**
     * For all characters between KEY_OPT_ASCII_START and KEY_OPT_ASCII_END
     * (inclusive) subtract this value from the key code to get the ASCII
     * character they represent.
     */
    public static final int KEY_OPT_ASCII_OFFSET = 0x10000; // 65536, 2^16
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_SPACE = KEY_OPT_ASCII_OFFSET+' ';
    // Sym Q
    public static final int KEY_OPT_ASCII_EXCLAMATION = KEY_OPT_ASCII_OFFSET+'!';
    // Sym L
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_DOUBLEQUOTE = KEY_OPT_ASCII_OFFSET+'\"';
    // Sym E
    public static final int KEY_OPT_ASCII_HASH = KEY_OPT_ASCII_OFFSET+'#';
    // Sym R
    public static final int KEY_OPT_ASCII_DOLLAR = KEY_OPT_ASCII_OFFSET+'$';
    // Sym T
    public static final int KEY_OPT_ASCII_PERCENT = KEY_OPT_ASCII_OFFSET+'%';
    // Sym U
    public static final int KEY_OPT_ASCII_AMPERSAND = KEY_OPT_ASCII_OFFSET+'&';
    // Sym K
    /** (Apostrophe) name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_QUOTE = KEY_OPT_ASCII_OFFSET+'\'';
    // Sym O
    public static final int KEY_OPT_ASCII_LPAREN = KEY_OPT_ASCII_OFFSET+'(';
    // Sym P
    public static final int KEY_OPT_ASCII_RPAREN = KEY_OPT_ASCII_OFFSET+')';
    // Sym I
    public static final int KEY_OPT_ASCII_ASTERISK = KEY_OPT_ASCII_OFFSET+'*';
    // Sym 1
    public static final int KEY_OPT_ASCII_PLUS = KEY_OPT_ASCII_OFFSET+'+';
    // Sym B
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_COMMA = KEY_OPT_ASCII_OFFSET+',';
    // Sym 2
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_MINUS = KEY_OPT_ASCII_OFFSET+'-';
    // Sym N
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_PERIOD = KEY_OPT_ASCII_OFFSET+'.';
    // Sym 5
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_SLASH = KEY_OPT_ASCII_OFFSET+'/';
    // 0-9 use KEY_NUM0-KEY_NUM9
    // Sym J
    public static final int KEY_OPT_ASCII_COLON = KEY_OPT_ASCII_OFFSET+':';
    // Sym H
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_SEMICOLON = KEY_OPT_ASCII_OFFSET+';';
    // Sym Z
    public static final int KEY_OPT_ASCII_LESS = KEY_OPT_ASCII_OFFSET+'<';
    // Sym 3
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_EQUALS = KEY_OPT_ASCII_OFFSET+'=';
    // Sym X
    public static final int KEY_OPT_ASCII_GREATER = KEY_OPT_ASCII_OFFSET+'>';
    // Sym M
    public static final int KEY_OPT_ASCII_QUESTION = KEY_OPT_ASCII_OFFSET+'?';
    // Sym W
    public static final int KEY_OPT_ASCII_AT = KEY_OPT_ASCII_OFFSET+'@';
    public static final int KEY_OPT_ASCII_A_UPPER = KEY_OPT_ASCII_OFFSET+'A';
    public static final int KEY_OPT_ASCII_Z_UPPER = KEY_OPT_ASCII_OFFSET+'Z';
    // Sym F
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_LBRACKET = KEY_OPT_ASCII_OFFSET+'[';
    // Sym V
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_BACKSLASH = KEY_OPT_ASCII_OFFSET+'\\';
    // Sym G
    /** name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_RBRACKET = KEY_OPT_ASCII_OFFSET+']';
    // Sym Y
    public static final int KEY_OPT_ASCII_CIRCUMFLEX = KEY_OPT_ASCII_OFFSET+'^';
    // Sym 7
    public static final int KEY_OPT_ASCII_UNDERSCORE = KEY_OPT_ASCII_OFFSET+'_';
    // Sym 9
    /** (Grave accent mark) name matches known network interface KEYBOARD command */
    public static final int KEY_OPT_ASCII_BACKQUOTE = KEY_OPT_ASCII_OFFSET+'`';
    public static final int KEY_OPT_ASCII_A_LOWER = KEY_OPT_ASCII_OFFSET+'a';
    public static final int KEY_OPT_ASCII_Z_LOWER = KEY_OPT_ASCII_OFFSET+'z';
    // Sym S
    public static final int KEY_OPT_ASCII_LBRACE = KEY_OPT_ASCII_OFFSET+'{';
    // Sym C
    public static final int KEY_OPT_ASCII_BAR = KEY_OPT_ASCII_OFFSET+'|';
    // Sym D
    public static final int KEY_OPT_ASCII_RBRACE = KEY_OPT_ASCII_OFFSET+'}';
    // Sym A
    public static final int KEY_OPT_ASCII_TILDE = KEY_OPT_ASCII_OFFSET+'~';
    /**
     * space character - beginning of range of values that are ASCII characters
     * plus KEY_OPT_ASCII_OFFSET. Note that 0-9 do not show up this way,
     * instead sending the KEY_NUM* codes.
     */
    public static final int KEY_OPT_ASCII_START = KEY_OPT_ASCII_SPACE;
    /**
     * tilde character - end of range of values that are ASCII characters plus
     * KEY_OPT_ASCII_OFFSET. Note that 0-9 do not show up this way,
     * instead sending the KEY_NUM* codes.
     */
    public static final int KEY_OPT_ASCII_END = KEY_OPT_ASCII_TILDE;
 
    // as discovered by davidblackledge - Premiere only.
    public static final int ID_FONT3_TTF = 13;
    public static final int ID_FONT3ITALIC_TTF = 14;
    public static final int ID_FONT3BOLD_TTF = 15;
    // as discovered by wmcbrine - Premiere only
    public static final int ID_SPEEDUP4_SOUND = 37;
 
    /**
     * HTTP Header sent in resource requests by the TiVo with the tsn (context
     * connection attribute "tsn") as value.
     */
    public static final String HTTP_HEADER_TIVO_ID = "TiVo_TCD_ID";
 
    /**
     * HTTP Header sent in resource requests by the TiVo with the TiVo Software
     * Version (Device Info event's "version") as value.
     */
    public static final String HTTP_HEADER_TIVO_VERSION = "TiVo_SW_VER";
 
    public static final String HTTP_HEADER_TIVO_DURATION = IHmeConstants.TIVO_DURATION;
 
}

V49BApplicationFactory.java

package com.blackledge.david.tivo;
 
import java.io.IOException;
 
import com.tivo.core.ds.ThreadUtils;
import com.tivo.hme.interfaces.IApplication;
import com.tivo.hme.interfaces.IContext;
import com.tivo.hme.interfaces.ILogger;
import com.tivo.hme.sdk.Factory;
import com.tivo.hme.sdk.IHmeProtocol;
import com.tivo.hme.sdk.io.HmeInputStream;
import com.tivo.hme.sdk.io.HmeOutputStream;
 
/**
 * Factory that creates the {@link V49BApplication} to report its version to the
 * TiVo as VERSION_0_49 and fixes HD UI problem with spaces in bonjour announced
 * App titles. (V49BApplication required for createApplication duplicate to have
 * correct method access).
 */
public class V49BApplicationFactory extends Factory {
    /**
     * Override to report a different HME version by calling
     * {@link #supercreateApplication(IContext, int)}.
     */
    @Override
    public IApplication createApplication(IContext context)
            throws IOException {
        int sentVersion = 
//                IHmeProtocol.VERSION;
                V49IHmeProtocol.VERSION_0_49;
//                IHmeProtocol.VERSION_0_40;
 
        IApplication application = supercreateApplication(context, sentVersion);
 
        return application;
    }
 
    /**
     * Copy of super factory implementation with parameterized version number to
     * send to TiVo.
     * Also fixes Factory's monitor getting changed to this instantiating application instance.
     */
    public IApplication supercreateApplication(IContext context, int sentVersion) throws IOException
    {
    int version = -1;
        HmeOutputStream out = null;
        HmeInputStream in = null;
 
        synchronized (lock) {                   // lock this factory
            if (context.getOutputStream() instanceof HmeOutputStream) {
                out = (HmeOutputStream)context.getOutputStream();
        }
            else {
                out = new HmeOutputStream(context.getOutputStream());
        }
 
            if (context.getInputStream() instanceof HmeInputStream) {
                in = (HmeInputStream)context.getInputStream();
            } else {
                in = new HmeInputStream(context.getInputStream());
        }
 
            // HME protocol starts here
        // Synchronize and flush the writes, so that no other writer 
        // interleaves within our data
        synchronized (out.getMonitor()) {
                out.writeInt(IHmeProtocol.MAGIC);
                out.writeInt(
//                        IHmeProtocol.VERSION
                        sentVersion
                        );
                out.flush();
        }
 
            // read magic and version
            int magic = in.readInt();
            if (magic != IHmeProtocol.MAGIC) {
                throw new IOException(
            "bad magic: 0x" + Integer.toHexString(magic)
//            +getIdForException());
            );
            }
            version = in.readInt();
            if (version >> 8 < 
//            IHmeProtocol.VERSION
            sentVersion
            >> 8 ) {
                throw new IOException(
                        "version mismatch: " + 
                        ( version >> 8 ) + "." + ( version & 0xff ) +
                        " < " +
                        ( 
//                                IHmeProtocol.VERSION
                                sentVersion
                                >> 8 ) + "." + ( 
//                                        IHmeProtocol.VERSION
                                        sentVersion
                                        & 0xff )
//            + getIdForException());
                        );
            }
        } // synchronized
 
        // maintain proper order of locks -- we have released 
        // the factory monitor before entering the app monitor
 
        IApplication retApp = null;
        try {
//            Application app = (Application)clazz.newInstance();
            // have to have local class to access setContext
            V49BApplication app = (V49BApplication)clazz.newInstance();
            synchronized (app) {
                app.setFactory(this);
                app.setContext(context, version);
        app.setMonitor(app);
//david.blackledge.com: undo thread set monitor for factory as app! stupid app.setMonitor!
ThreadUtils.obj.setMonitorFor(this, this);
                retApp = app;
            }
        } catch (InstantiationException ex) {
        log(ILogger.LOG_NOTICE, ex);
        } catch (IllegalAccessException ex) {
        log(ILogger.LOG_NOTICE, ex);
        }
 
    out.flush();
 
        return retApp;
    } // createApplication()
 
    /**
     * Ensures any spaces (ASCII 32, 0x20) are replaced with non-breaking spaces (0xA0 or ASCII 160)
     */
    protected String fixTitle(String origTitle) {
        return origTitle.replace((char)0x20, (char)0xA0);
    }
 
    /**
     * {@inheritDoc}
     * Ensures any spaces (ASCII 32, 0x20) are replaced with non-breaking spaces (0xA0 or ASCII 160)
     */
    @Override
    public void setAppTitle(String title) {
        super.setAppTitle(fixTitle(title));
    }
 
    /**
     * {@inheritDoc}
     * Ensures any spaces (ASCII 32, 0x20) are replaced with non-breaking spaces (0xA0 or ASCII 160)
     */
    @Override
    public String getAppTitle() {
        return fixTitle(super.getAppTitle());
    }
}

V49BApplication.java

package com.blackledge.david.tivo;
 
import java.awt.Color;
import java.awt.Rectangle;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.URLDecoder;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import com.tivo.hme.bananas.BApplication;
import com.tivo.hme.interfaces.IContext;
import com.tivo.hme.sdk.Application;
import com.tivo.hme.sdk.HmeEvent;
import com.tivo.hme.sdk.HmeEvent.DeviceInfo;
import com.tivo.hme.sdk.IHmeProtocol;
import com.tivo.hme.sdk.Resource;
import com.tivo.hme.sdk.Version;
 
/**
 * BApplication that fixes SDK bugs, supports some features beyond SDK 1.4.1 and
 * makes some things easier. See {@link V49BApplicationFactory} and
 * {@link V49IHmeProtocol} for other features.
 * <UL>
 * <LI>Use {@link V49BApplicationFactory} as superclass for your factory to tell the TiVo you're using V49 (or another version).
 * <LI>Constants for SDK resource strings *.ttf, *.snd
 * <LI>Constants for discovered Stream resources "livetv:" "loopset:" and
 * "recording:"
 * <LI>Getters for spacing when a "loopset:" is the background
 * <LI>Member variables for protocol version, tivo version, and tivo ID.
 * <LI>Resolution help: automatically set preferred resolution, template methods
 * for init and showing the first BScreen during resolution setting
 * <LI>Template method for cleaning up during {@link #destroy()} to prevent an
 * SDK 1.4 bug (not an issue in 1.4.1e)
 * <LI>Post Resource errors to the resource and with new version of
 * {@link #handleApplicationError(int, String, Integer)}
 * <LI>Fix SDK bug that broke Application-as-stream feature.
 * <LI>Support more TTF built-in fonts with new constant names for series-4-only
 * fonts, and with id numbers-as-font-names for arbitrary attempts.
 * <LI>SDK capability/version methods.
 * <LI>Backward compatibility of new SDK methods to old SDK. You should be able
 * to run this app in either SDK.
 * </UL>
 * 
 * @author David.Blackledge.com
 * 
 */
public abstract class V49BApplication extends BApplication implements V49IHmeProtocol {
 
    public static final String TTF_SERIES4FONT3BOLD = "series4font3bold.ttf"; //$NON-NLS-1$
    public static final String TTF_SERIES4FONTITALIC = "series4fontitalic.ttf"; //$NON-NLS-1$
    public static final String TTF_SERIES4FONT = "series4font.ttf"; //$NON-NLS-1$
    public static final String TTF_DEFAULT = "default.ttf"; //$NON-NLS-1$
    public static final String TTF_SYSTEM = "system.ttf"; //$NON-NLS-1$
 
    public static final String SOUND_BONK = "bonk.snd"; //$NON-NLS-1$
    public static final String SOUND_UPDOWN = "updown.snd"; //$NON-NLS-1$
    public static final String SOUND_THUMBSUP = "thumbsup.snd"; //$NON-NLS-1$
    public static final String SOUND_THUMBSDOWN = "thumbsdown.snd"; //$NON-NLS-1$
    public static final String SOUND_SELECT = "select.snd"; //$NON-NLS-1$
    public static final String SOUND_TIVO = "tivo.snd"; //$NON-NLS-1$
    public static final String SOUND_LEFT = "left.snd"; //$NON-NLS-1$
    public static final String SOUND_RIGHT = "right.snd"; //$NON-NLS-1$
    public static final String SOUND_PAGEUP = "pageup.snd"; //$NON-NLS-1$
    public static final String SOUND_PAGEDOWN = "pagedown.snd"; //$NON-NLS-1$
    /** Note, this sounds identical to {@link #SOUND_THUMBSUP} */
    public static final String SOUND_ALERT = "alert.snd"; //$NON-NLS-1$
    public static final String SOUND_DESELECT = "deselect.snd"; //$NON-NLS-1$
    public static final String SOUND_ERROR = "error.snd"; //$NON-NLS-1$
    public static final String SOUND_SLOWDOWN1 = "slowdown1.snd"; //$NON-NLS-1$
    public static final String SOUND_SPEEDUP1 = "speedup1.snd"; //$NON-NLS-1$
    public static final String SOUND_SPEEDUP2 = "speedup2.snd"; //$NON-NLS-1$
    public static final String SOUND_SPEEDUP3 = "speedup3.snd"; //$NON-NLS-1$
// not yet supported by SDK or these V49 classes
//    public static final String SOUND_SERIES4SPEEDUP4 = "speedup4.snd"; //$NON-NLS-1$
 
    /**
     * TiVo Built-in video stream resource which displays the "current" live tv
     * tuner, defaulting to the end of the 30 minute buffer.
     */
    public static final String STREAM_LIVETV = "livetv:"; //$NON-NLS-1$
    /**
     * TiVo Built-in background resource used on "TiVo Central" page. Red
     * animated video in Series 3 and earlier, Blue image in Series and later.
     */
    public static final String STREAM_LOOP_RED_CENTRAL = "loopset:Central"; //$NON-NLS-1$
    /**
     * TiVo Built-in background resource used on "Showcases" and
     * "TiVo Suggestions" pages. Purple animated video in Series 3 and earlier,
     * Blue image in Series and later.
     */
    public static final String STREAM_LOOP_PURPLE_SHOWCASES = "loopset:Showcases"; //$NON-NLS-1$
    /**
     * TiVo Built-in background resource used on "Setup" and "Apps/Photos/Music"
     * pages. Blue animated video in Series 3 and earlier, Blue image in Series
     * and later.
     */
    public static final String STREAM_LOOP_BLUE_SETUP = "loopset:Setup"; //$NON-NLS-1$
    /**
     * TiVo Built-in background resource used on "Now Playing" and all pages
     * that don't use the other LOOP resources. Green animated video in Series 3
     * and earlier, Blue image in Series and later.
     */
    public static final String STREAM_LOOP_GREEN_NOWPLAYING = "loopset:NowPlaying"; //$NON-NLS-1$
    public static final String STREAM_RECORDING_PREFIX = "recording:"; //$NON-NLS-1$
 
    /**
     * Protocol in use between the TiVo and the server. Used in methods like
     * {@link #isSlideRemoteSupported()}. Accurate/dynamic version of
     * {@link IHmeProtocol#VERSION}. Saved during
     * {@link #setContext(IContext, int)} call. Use V49BapplicationFactory as
     * your factory's supertype to register V49 as your preferred protocol
     * version.
     */
    public int protocolVersion;
    /**
     * Device Info event's "version" - the TiVo's Software Version. E.g.
     * 11.0k-01-2-652 . Available after DEVICE_INFO_EVENT comes in (equivalent
     * to information in {@link V49IHmeProtocol#HTTP_HEADER_TIVO_VERSION}).
     */
    public String tivoVersion;
    /**
     * The TiVo Service Number (tsn) uniquely identifying the connected TiVo DVR
     * device. Available after {@link #setContext(IContext, int)} is called
     * (extracted from {@link V49IHmeProtocol#HTTP_HEADER_TIVO_ID}).
     */
    public String tivoID;
    /**
     * Map from the DeviceInfo event. 
     * Examples:
     * <LI>host=Bedroom (user-defined or system default with last 4 of TSN)
     * <LI>brand=TiVo (or e.g. Pace)
     * <LI>platform=Gen06 (Gen07,Gen08,Gen10,Pac01)
     * <LI>version=11.0k-01-2-652
     * <LI>zoneinfo=America/Chicago
     * <LI>country=US (or e.g. CA)
     * <LI>language=enUS
     * <LI>active-ui-mode=classic (or flash)
     * <LI>com.tivo.examine.idType.stationId=tivo
     * <LI>com.tivo.examine.showing=true
     * <LI>com.tivo.examine.idType.contentId=tivo
     * <LI>credential-manager-id-ccid=(Hex number up to 8 digits)
     * <LI>credential-manager-id-mac=(12dig hex number)
     * <LI>has-recording-capability=true
     * <LI>has-recording-storage=true (false seen)
     * <LI>has-season-pass-capability=true
     * <LI>vod-session-capable=false
     */
    public Map deviceInfo;
    /**
     * TimeZone based on DeviceInfo's "zoneinfo" id, or null (if not found, or
     * if result is not-found default id of "GMT") - (you could use
     * TimeZone.getDefault()). Null until DeviceInfo event arrives.
     */
    public TimeZone deviceZone;
 
    /**
     * Does nothing.  Override this to output debug messages
     * @param message
     */
    public void debug(String message) {
    }
 
    /**
     * Template method to do {@link #init(IContext)} tasks between super's init
     * and preferred resolution setting. Better to override this than init
     * unless you want to handle setting resolution yourself.
     */
    protected void initBeforeResolutionSet(IContext context) {
    }
 
    /**
     * Template method to do {@link #rootBoundsChanged(Rectangle)} tasks between
     * super's rootBoundsChanged and a possible call to {@link #showFirstPage()}
     * if this is the first root bounds change during application
     * initialization. Override this, or it might be OK to override
     * rootBoundsChanged and just call its super method and ignore this.
     * 
     * @param rootBounds
     */
    protected void rootBoundsChangedBeforeFirstPage(Rectangle rootBounds) {
    }
 
    /**
     * Override this to push the first BScreen of the application after
     * resolution issues are resolved.
     */
    protected abstract void showFirstPage();
 
    /**
     * Override this instead of {@link #destroy()} for SDK 1.4 to do safe cleanup during the
     * destroy operation. Prevents the loop that occurs if a fatal error happens
     * during destroy.  Not an issue in 1.4.1e
     */
    protected void destroyCleanup() {
    }
 
    /* BEGIN fix JDK */
 
    // TODO future: handle unsupported chunks (e.g. Event 10)
    // public boolean handleChunk(InputStream in)
    // {
    // synchronized (lock) {
    // //debug("handleChunk()");
    // boolean doMore = true;
    //
    // // flush any data that was generated from previous handling of
    // // event
    // flush();
    //
    // ChunkedInputStream chunkInStr = null;
    // if (in instanceof ChunkedInputStream)
    // {
    // chunkInStr = (ChunkedInputStream)in;
    // }
    // else
    // {
    // chunkInStr = new ChunkedInputStream(in); //,
    // IHmeConstants.TCP_BUFFER_SIZE);
    // }
    //
    // // if ( protocolVersion < VERSION_0_40 )
    // // {
    // // chunkInStr.setUseVString( false );
    // // }
    // // else {
    // chunkInStr.setUseVString( true );
    // // }
    //
    // int opcode = -1;
    // try {
    // opcode = (int)chunkInStr.readVInt();
    // } catch (IOException e) {
    // // receiver closed - ignore
    // // if (debug()) {
    // // log(ILogger.LOG_DEBUG,
    // // "Connection terminated by receiver");
    // // }
    // }
    // if (opcode == -1) {
    // doMore = false;
    // return doMore;
    // }
    //
    // HmeEvent evt = null;
    // String eventString = null;
    // try
    // {
    // switch (opcode) {
    // case EVT_DEVICE_INFO:
    // evt = new HmeEvent.DeviceInfo(chunkInStr);
    // break;
    // case EVT_APP_INFO:
    // evt = new HmeEvent.ApplicationInfo(chunkInStr);
    // break;
    // case EVT_RSRC_INFO:
    // evt = new HmeEvent.ResourceInfo(chunkInStr, this);
    // break;
    // case EVT_RESOLUTION_INFO:
    // evt = new HmeEvent.ResolutionInfo(chunkInStr);
    // break;
    // case EVT_KEY:
    // evt = new HmeEvent.Key(chunkInStr);
    // break;
    // case EVT_IDLE:
    // evt = new HmeEvent.Idle(chunkInStr);
    // break;
    // case EVT_FONT_INFO:
    // evt = new HmeEvent.FontInfo(chunkInStr);
    // break;
    // case EVT_INIT_INFO:
    // evt = new HmeEvent.InitInfo(chunkInStr);
    // break;
    // default:
    // eventString =
    // "HmeEvent[opcode=" + opcode
    // + ", id = " + chunkInStr.readVInt()
    // + "]";
    // // short clen = chunkInStr.readShort();
    // // while (clen != 0) {
    // // System.out.println(clen);
    // // chunkInStr.skip(clen);
    // // clen = chunkInStr.readShort();
    // // }
    //
    // // System.out.println(eventString);
    // // try {
    // // System.out.println(chunkInStr.readInt());
    // // System.out.println(chunkInStr.readDict());
    // // } catch(IOException e1) {
    // // chunkInStr.readTerminator();
    // // throw e1;
    // // }
    // break;
    // }
    // if (evt != null) {
    // evt.setMonitor(lock);
    // }
    // chunkInStr.readTerminator();
    // }
    // catch (IOException e) {
    // evt = null;
    // getLogger().log(ILogger.LOG_INFO, e);
    // }
    //
    // //
    // // Save the event, in case we need to dump the list
    // //
    // // if (eventsReceived != null) {
    // // if (eventsReceived.size() >= MAX_RECEIVED_EVENTS_SIZE) {
    // // // Startup is complete -- stop checking for protocol errs
    // // startupComplete = true;
    // // eventsReceived.clear();
    // // eventsReceived = null;
    // // } else {
    // // if (evt != null) {
    // // eventString = evt.toString();
    // // }
    // // if (eventString == null) {
    // // eventString = "Bad HmeEvent[opcode="+opcode+", no ID]";
    // // }
    // // eventsReceived.add(eventString);
    // // }
    // // }
    //
    // if (evt == null) {
    // getLogger().log(ILogger.LOG_DEBUG,
    // "unknown event opcode : " + opcode);
    // }
    // else
    // {
    // // if any debugging is turned on, verify the event is kosher
    // // if (debug()) {
    // // evt.verify(logger);
    // // }
    //
    // // debugEvents means to print each and every event received
    // // if (debugEvents()) {
    // getLogger().log(ILogger.LOG_DEBUG, "event " + evt);
    // // }
    //
    // // if (!preprocessEvent(evt)) {
    // //
    // // // preprocessEvent() says this event should be discarded
    // // if (eventsReceived != null) {
    // // int i = eventsReceived.size() - 1;
    // // eventsReceived.set(i,
    // // eventsReceived.get(i).toString() + " [IGNORED]");
    // // }
    //
    // // } else {
    //
    // // The app should receive this event.
    // //
    // // Only dispatch events after app.init() is called.
    // // Save events before init(), to be dispatched later.
    // //
    // // if (appInitialized) {
    // // dispatchEvent(evt);
    // // } else if (queuedEvents != null) {
    // // // Save the events to send after initialization
    // // queuedEvents.add(evt);
    // // }
    // // }
    // }
    //
    // // flush any data that was generated from handling of event
    // flush();
    //
    // return doMore;
    // } // sync
    //
    // } // handleChunk()
 
    /**
     * unfortunately sdk's Resource.ResourceStatus isn't public, so we recreate
     * it here - all it does is use a different Event ID
     */
    protected static class MockResourceStatus extends HmeEvent.ResourceInfo {
        MockResourceStatus(int id, Resource rsrc, int status, Map map) {
            super(EVT_RSRC_STATUS, id, rsrc, status, map);
        }
 
        public String toString() {
            return getID() + ".RSRC_STATUS(" + statusToString(getStatus()) //$NON-NLS-1$
                    + ")"; //$NON-NLS-1$
        }
    }
 
    private boolean destroyedAlready = false;
 
    public void destroy() {
        // protect against SDK 1.4 bug: if there is a fatal error during below
        // operations (e.g. remove operations) close gets called which calls
        // destroy again.
 
        // what happens: after the 2nd call to here, we'll get HMEException from
        // the remove call that caused it which contains the actual cause
        // (unless it was originally an HMEException)
        // setId(-1) has not been called on the object yet
 
        // SO: want to catch HMEExceptions if we want to finish our destroy
        // operation
        if (destroyedAlready)
            return;
        destroyedAlready = true;
 
        // template method
        destroyCleanup();
        // ... operations that could cause a fatal error, e.g. "remove" calls
 
        super.destroy(); // in case e.g. BApplication ever starts doing
                            // something there.
    }
 
    /**
     * Improved version of
     * {@link Application#handleApplicationError(int, String)} that includes the
     * resourceId related to the error - critical for knowing when you have a
     * problem with a resource you created. Default implementation simply calls
     * {@link #handleApplicationError(int, String)}. ApplicationError codes 1
     * (e.g. "can't create rsrc. unsupported stream type
     * /avatar/cf1e61a4330e75d5d1d7a744c5ef38c4") and 3 (e.g. "resource 2091 not
     * found (type type[-1])") about a resource never provide a resourceinfo
     * event!
     * 
     * @param errorCode
     *            the error code of the error (one of the {@link IHmeProtocol}
     *            APP_ERROR_ constants)
     * @param errorText
     *            details of the error
     * @param resourceId
     *            the id associated with the error - could be null. Suitable for
     *            use with {@link #getResource(Object)} (or ((WeakReference)
     *            {@link #getResources()}.get(Object)).get() to avoid automatic
     *            SDK warning message when it's not found)
     * @return true if the event was handled.
     */
    public boolean handleApplicationError(int errorCode, String errorText, Integer resourceId) {
        if (handleApplicationError(errorCode, errorText)) {
            return true;
        } else {
            // FIXME add processing needed by e.g. image exceeding max width or
            // height (errorCode 2) with no resource id
            return false;
        }
    }
 
    /**
     * Perform some common handling - be sure to call this from subclasses that
     * override it. Sets {@link #tivoVersion} from DEVICE_INFO. Improves error
     * handling by posting resource errors to the resource itself, and calling
     * {@link #handleApplicationError(int, String, Integer)}, passing in
     * resource ID.
     * 
     */
    public boolean handleEvent(HmeEvent event) {
        // get tivo version from device info
        if (event.getOpCode() == EVT_DEVICE_INFO) {
            HmeEvent.DeviceInfo info = (DeviceInfo) event;
 
            this.tivoVersion = (String) info.getMap().get("version"); //$NON-NLS-1$
 
            try {
                this.deviceZone = TimeZone.getTimeZone((String) info.getMap().get("zoneinfo"));
                //TODO assuming GMT is not right. (it's the default if the supplied zone is not found)
                if(this.deviceZone == null || this.deviceZone.getID().equals("GMT")) {
                    debug("TimeZone object: ignore GMT");
                    this.deviceZone = null;
                }
            } catch(Exception e) {
                this.deviceZone = null;
            }
            debug("TimeZone object: "+(this.deviceZone==null?null:this.deviceZone.getID()));
 
            this.deviceInfo = info.getMap();
        }
 
        // ...
        if (event.getOpCode() == EVT_APP_INFO) {
            HmeEvent.ApplicationInfo info = (HmeEvent.ApplicationInfo) event;
            if (info.getMap().get("error.code") != null) { //$NON-NLS-1$
                int errorCode = Integer.parseInt((String) info.getMap().get(
                        "error.code")); //$NON-NLS-1$
                Integer resourceId = null;
                if (info.getMap().get("error.rsrc") != null) { //$NON-NLS-1$
                    try {
                        resourceId = new Integer((String) info.getMap().get(
                                "error.rsrc")); //$NON-NLS-1$
                    } catch (NumberFormatException e) {
                        resourceId = null;
                    }
                }
                String errorText = (String) info.getMap().get("error.text"); //$NON-NLS-1$
                if (resourceId != null) {
                    WeakReference res_wr = (WeakReference) getResources().get(
                            resourceId);
                    if (res_wr != null && res_wr.get() != null) {
                        Resource res = (Resource) res_wr.get();
                        Map resInfoMap = info.getMap();
                        // TODO we're doing this regardless of whether it's
                        // actually a change in status
                        if (res.status == Resource.RSRC_STATUS_ERROR) {
                            debug("NOTE: had to send ResourceStatus event for error even though resource's status was already error."); //$NON-NLS-1$
                        }
                        debug("Posting APP_INFO resource error to resource " //$NON-NLS-1$
                                + resourceId + ": " + resInfoMap); //$NON-NLS-1$
                        res.postEvent(new MockResourceStatus(resourceId
                                .intValue(), res, Resource.RSRC_STATUS_ERROR,
                                resInfoMap));
                    }
                } else
                    debug("APP_INFO error " + errorCode + ": " + errorText); //$NON-NLS-1$ //$NON-NLS-2$
 
                return handleApplicationError(errorCode, errorText, resourceId);
            }
        }
 
        boolean result = super.handleEvent(event);
        // ...
 
        return result;
    }
 
    /**
     * SDK Bug Fix - OMG! sick bug in HMEObject that has never been noticed or
     * exercised since nobody has apparently attempted to do App-in-app with
     * {@link #createStream(String, Map)}. They forgot a "new" keyword and
     * auto-"fixed" the problem creating a method named LinkedHashMap that has
     * default method content returning null! Also, calling code won't handle
     * the nulls that are returned in certain circumstances, so now returning an
     * empty map here. Lucky for us it is public and non-final. {@inheritDoc}
     * 
     * @see com.tivo.hme.sdk.HmeObject#parseQuery(java.lang.String)
     */
    public Map parseQuery(String query) {
        Map map = new LinkedHashMap();
        if (query == null || query.indexOf('=') == -1) {
            return map;
        }
 
        int at = 0;
        int len = query.length();
 
        if (query.startsWith("?")) { //$NON-NLS-1$
            ++at;
        }
        do {
            // find = and &
            int equal = query.indexOf('=', at);
            if (equal < 0) {
                throw new IllegalArgumentException(
                        "invalid query (trailing key, = not found)"); //$NON-NLS-1$
            }
            int amp = query.indexOf('&', equal);
            if (amp == -1) {
                amp = query.length();
            }
 
            // add the key/value
            try {
                String key = URLDecoder.decode(query.substring(at, equal),
                        "UTF-8"); //$NON-NLS-1$
                String value = URLDecoder.decode(
                        query.substring(equal + 1, amp), "UTF-8"); //$NON-NLS-1$
                map.put(key.toLowerCase(), value);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
 
            // advance
            at = amp + 1;
        } while (at < len);
        return map;
    }
 
    /**
     * Override to additionally support the STREAM_* constants to call
     * createStream, and to support a new text creation shortcut (like animation
     * and color) when a String key starts with an apostrophe.
     */
    @Override
    public Resource getResource(Object key) {
        if (key instanceof String) {
            if (key.toString().equals(STREAM_LOOP_BLUE_SETUP)
                    || key.toString().equals(STREAM_LOOP_GREEN_NOWPLAYING)
                    || key.toString().equals(STREAM_LOOP_PURPLE_SHOWCASES)
                    || key.toString().equals(STREAM_LOOP_RED_CENTRAL)
                    || key.toString().equals(STREAM_LIVETV)
                    || key.toString().startsWith(STREAM_RECORDING_PREFIX)) {
                return createStream(key.toString());
            }
            if (createTextPattern.matcher(key.toString()).matches()) {
                return createText(key.toString());
            } else if(key.toString().indexOf(".font:")>0){
                System.out.println("NOPE:"+key);
            }
        }
        return super.getResource(key);
    }
    /* END fix JDK */
 
    private Pattern createTextPattern = Pattern.compile("(?:java.awt.Color)?\\[([^\\]]*)(?:\\]([^:]*)(?::(.*))?)?", Pattern.DOTALL);
 
    /**
     * Creates a text resource using a specially formatted string, or using
     * default color (white) and font (default-15.font).<BR/>
     * <B>Supported createText shortcut format:</B><BR/>
     * A string that begins with [ or a Color.toString() is a createText
     * shortcut. Color is specified between the brackets, font is specified
     * after the close bracket and before a colon, text is after the colon.
     * Color defaults to white (0xFFFFFF), and font defaults to default-15.font.
     * Shortcut using just defaults is text after a [ (as long as the text
     * doesn't have a close bracket in it). Equivalent examples:<BR/>
     * "[0xFFFFFF]default-15.font:Hello World" or "[]:Hello World" or
     * "[]Hello World" or "[Hello World" or Color.white+"Hello World" or
     * Color.white+":Hello World" or Color.white+"default-15.font:Hello World" <BR/>
     * 
     * @param stringSpec
     *            [color]font:text, [text, []text, []:text, [color]:text,
     *            java.awt.Color[color]:text, []font:text, or
     *            java.awt.Color[color]font:text where color is 0xHHHHHH or
     *            r=DDD,g=DDD,b=DDD
     */
    public Resource createText(String stringSpec) {
        Object color = Color.white;
        String font = "default-15.font";
 
        Matcher matcher = createTextPattern.matcher(stringSpec);
        if (matcher.matches() && matcher.groupCount() == 3) {
            String sfont = null;
            Object scolor = null;
            if (matcher.group(2) == null) {
                stringSpec = matcher.group(1);
            } else if (matcher.group(3) == null) {
                scolor = parseColor(matcher.group(1).trim());
                stringSpec = matcher.group(2);
            } else {
                scolor = parseColor(matcher.group(1).trim());
                sfont = matcher.group(2).trim();
                stringSpec = matcher.group(3);
            }
 
            if (scolor != null) {
                color = scolor;
            }
            if (sfont != null && sfont.length() > 0) {
                if (sfont.endsWith(".font")) {
                    font = sfont;
                }
            }
        }
        return createText(font, color, stringSpec);
    }
 
    private Pattern colorToStringPattern = Pattern.compile("r=(\\d+),g=(\\d+),b=(\\d+)");        
    private Object parseColor(String color) {
        if (color.startsWith("0x")) {
            return color;
        }
 
        Matcher matcher = colorToStringPattern.matcher(color);
        if (matcher.matches()) {
            try {
                if (matcher.groupCount() == 3) {
                    return new Color(Integer.parseInt(matcher.group(1)),
                            Integer.parseInt(matcher.group(2)),
                            Integer.parseInt(matcher.group(3)));
                }
            } catch(Exception e) {
                return color;
            }
        }
        if(color.trim().length() == 0) {
            return null;
        }
        return color;
    }
 
    /**
     * Override to handle additional system font ids as [id].ttf and additional
     * names.
     */
    public Resource createTrueType(String family) {
        // automatically handle 10.ttf - 19.ttf as ID refs
        if (family != null && family.matches("\\d+\\.ttf")) {
            try {
                int id = Integer.parseInt(family.substring(0, family.length()-4));
                if (id < 20 && id >= 10) {
                    return new TrueTypeFalseResource(this, family, id);
                }
                if(id >= 20) {
                    return getResource(new Integer(id));
                }
            } catch (Exception e) {
            }
        }
        // handle specific new names
        if (family.equals(TTF_SERIES4FONT)) {
            return new TrueTypeFalseResource(this, family, 13);
        } else if (family.equals(TTF_SERIES4FONTITALIC)) {
            return new TrueTypeFalseResource(this, family, 14);
        } else if (family.equals(TTF_SERIES4FONT3BOLD)) {
            return new TrueTypeFalseResource(this, family, 15);
//        } else if... {
        } else {
            // default behavior
            return super.createTrueType(family);
        }
    }
 
    /**
     * Equivalent of TrueTypeResource except accessible to this class so we can
     * instantiate additional names with a supplied id
     */
    private static class TrueTypeFalseResource extends Resource {
        String name;
 
        TrueTypeFalseResource(Application app, String name, int id) {
            super(app, id);
            this.name = name;
            ((V49BApplication)app).debug("Created font " + name + " with id " + id);
        }
 
        /**
         * This is a thread-safe public entry point.
         */
        protected void toString(StringBuffer buf) {
            synchronized (lock) {
                buf.append(",ttf=" + name);
            }
        }
    }
 
    private boolean waitingToInit;
 
    @Override
    protected void setContext(IContext context, int version) {
        super.setContext(context, version);
        // make accessible version of private Application class variable.
        this.protocolVersion = version;
        debug("Protocol Version per TiVo DVR: " + protocolVersion); //$NON-NLS-1$
 
        debug("attributes: " + context.getConnectionAttributes()); //$NON-NLS-1$
        // User-Agent: TvHttpClient -my tivo (and tsn=64...3E )
        // User-Agent: TmkHttpRequest/1.0 - from somebody on internet - old
        // version of tivo?
        // User-Agent=tivo.http/03/30/2003 - simulator
        // attributes: {Cookie=id=895df24b5038b953631128d3aa994863d51da2b1,
        // Host=192.168.0.20:7288, User-Agent=TvHttpClient, Connection=close,
        // tsn=64...3E}
        this.tivoID = context.getConnectionAttribute("tsn"); //$NON-NLS-1$
        // context.getConnectionAttribute(HTTP_HEADER_TIVO_ID);
        // tivoVersion is in DEVICE_INFO event under "version"
        // this.tivoVersion =
        // context.getConnectionAttribute(HTTP_HEADER_TIVO_VERSION);
 
    }
 
    /**
     * You should override {@link #initBeforeResolutionSet(IContext)} to do your
     * init work while this override handles resolution details after that is
     * called.
     */
    @Override
    public void init(IContext context) throws Exception {
        super.init(context);
        setActive(true);
 
        // template method for subclasses to perform init activities
        initBeforeResolutionSet(context);
 
        waitingToInit = true;
        boolean changed = setPreferredResolution();
        // will continue init on rootBoundsChanged()
 
        this.setActive(true);
 
        if (!changed || !isResolutionSupported()) {
            waitingToInit = false;
            showFirstPage();
        }
 
    }
 
    /**
     * This override coordinates with the {@link #init(IContext)} override to
     * call {@link #showFirstPage()} at the right time. Should probably override
     * {@link #rootBoundsChangedBeforeFirstPage(Rectangle)} instead of this to
     * maintain order of calls.
     */
    @Override
    public void rootBoundsChanged(Rectangle rootBounds) {
        super.rootBoundsChanged(rootBounds);
 
        // template method for subclasses to perform rootbounds activities? maybe just subclass calling super is good enough here.
        rootBoundsChangedBeforeFirstPage(rootBounds);
 
        if (isResolutionSupported()) {
            if (waitingToInit) {
                waitingToInit = false;
                showFirstPage();
            }
        }
    }
 
    /**
     * Resolution control is supported in protocol version 0.43 and SDK 1.4.1
     */
    public boolean isResolutionSupported() {
        return this.protocolVersion >= VERSION_0_43 && isSDK141();
    }
 
    /**
     * Stream Map argument is supported in protocol version 0.45 and SDK 1.4.1
     */
    public boolean isStreamMapSupported() {
        return this.protocolVersion >= VERSION_0_45 && isSDK141();
    }
 
    /**
     * Slide Remote is supported in protocol version 0.49
     */
    public boolean isSlideRemoteSupported() {
        return this.protocolVersion >= VERSION_0_49;
    }
 
    /**
     * Tries to get the version string from the com.tivo.hme.sdk.Version class
     * which doesn't exist in all SDK versions. Returns null if it can't find
     * the class.
     * 
     * @return
     */
    public static String getSDKVersion() {
        try {
            if (Class.forName("com.tivo.hme.sdk.Version") != null) //$NON-NLS-1$
                return Version.getVersion();
            else
                return null;
        } catch (ClassNotFoundException e) {
            return null;
        }
    }
 
    public static boolean isSDK141() {
        if (getSDKVersion() != null)
            return getSDKVersion().startsWith("1.4.1"); //$NON-NLS-1$
        else
            return false;
    }
 
    /** a multi-SDK version of the SDK 1.4.1 method */
    public int getSafeActionHorizontal() {
        if (!isSDK141()) {
            return SAFE_ACTION_H;
        } else {
            return super.getSafeActionHorizontal();
        }
    }
 
    /** a multi-SDK version of the SDK 1.4.1 method */
    public int getSafeTitleVertical() {
        if (!isSDK141()) {
            return SAFE_TITLE_V;
        } else {
            return super.getSafeTitleVertical();
        }
    }
 
    /** a multi-SDK version of the SDK 1.4.1 method */
    public int getSafeActionVertical() {
        if (!isSDK141()) {
            return SAFE_ACTION_V;
        } else {
            return super.getSafeActionVertical();
        }
    }
 
    /** a multi-SDK version of the SDK 1.4.1 method */
    public int getSafeTitleHorizontal() {
        if (!isSDK141()) {
            return SAFE_TITLE_H;
        } else {
            return super.getSafeTitleHorizontal();
        }
    }
 
    /** Standard title area for e.g. a STREAM_LOOP_* background. */
    public Rectangle getTitleRegion() {
        int topPad = getSafeTitleVertical();
        int height = getSafeTitleToScrollTop()
                - 15;
        int leftPad = getSafeTitleHorizontal() + getSafeLogoToScrollTitle();
        int rightPad = getSafeTitleHorizontal();
        return new Rectangle(leftPad, topPad, getWidth()-leftPad-rightPad, height);
    }
 
    /** Standard logo area for e.g. a STREAM_LOOP_* background. */
    public Rectangle getLogoRegion() {
        int topPad = getSafeTitleVertical();
        int height = getSafeTitleToScrollTop()
                - 15;
        int leftPad = getSafeTitleHorizontal()
                - 20;
        int width = getSafeLogoToScrollTitle() + 10; // seems to fit, but shouldn't it be +20 to offset leftpad adjustment?
        return new Rectangle(leftPad, topPad, width, height);
    }
 
    /** Standard scroll area for e.g. a STREAM_LOOP_* background. */
    public Rectangle getScrollRegion() {
        int bottomPad = getSafeTitleVertical()+getSafeTitleToScrollBottom();
        int topPad = getSafeTitleVertical()+getSafeTitleToScrollTop();
        int leftPad = getSafeTitleHorizontal()+17; // pad 17(for hd at least) to get "inside" the scroll region like the top is
        int rightPad = getSafeTitleHorizontal();
        return new Rectangle(leftPad, topPad, getWidth()-leftPad-rightPad, getHeight()-topPad-bottomPad);
    }
 
    /** Region based {@link #getSafeTitleHorizontal()} and {@link #getSafeTitleVertical()} */
    public Rectangle getSafeTitleRegion() {
        return new Rectangle(getSafeTitleHorizontal(), getSafeTitleVertical(),
                getWidth() - 2 * getSafeTitleHorizontal(), 
                getHeight() - 2 * getSafeTitleVertical());
    }
 
    /** Region based {@link #getSafeActionHorizontal()} and {@link #getSafeActionVertical()} */
    public Rectangle getSafeActionRegion() {
        return new Rectangle(getSafeActionHorizontal(), getSafeActionVertical(),
                getWidth() - 2 * getSafeActionHorizontal(), 
                getHeight() - 2 * getSafeActionVertical());
    }
 
    /**
     * Vertical Space from safeTitle bottom to bottom of scrolling area of of a
     * STREAM_LOOP_* background.
     */
    public int getSafeTitleToScrollBottom() {
        if (isResolutionSupported() && getCurrentResolution().getHeight() > 480) {
            return 20;
        } else {
            return 15; // SD number
        }
    }
 
    /**
     * Vertical Space from safeTitle top to top of scrolling area of a
     * STREAM_LOOP_* background.
     */
    public int getSafeTitleToScrollTop() {
        if (isResolutionSupported() && getCurrentResolution().getHeight() > 480) {
            return 85 + 60;
        } else {
            return 95; // SD number
        }
    }
 
    /**
     * Horizontal space from safeTitle left to right edge of TiVo logo in the
     * title area of a STREAM_LOOP_* background.  Note, logo extends a little to the left of the safeTitle left.
     */
    public int getSafeLogoToScrollTitle() {
        if (isResolutionSupported() && getCurrentResolution().getHeight() > 480) {
            return 100; // could need a diff number for 720, but this will work.
        } else {
            return 45;// SD number - goes past "glow"
        }
    }
 
    protected void setMaxStackDepth(int max) {
        if (isSDK141()) {
            super.setMaxStackDepth(max);
        }
    }
 
    /**
     * If {@link #isResolutionSupported()}, makes sure the resolution is the
     * preferred one, and if it's not, calls
     * {@link #setReceiverResolution(com.tivo.hme.sdk.Resolution)} with the
     * ResolutionInfo's preferred resolution.
     * 
     * @return true if {@link #setReceiverResolution(com.tivo.hme.sdk.Resolution)} was actually called.
     */
    protected boolean setPreferredResolution() {
        /* BEGIN HME 1.4.1e only */
        if (isResolutionSupported()) {
            boolean noChangeNeeded = this
                    .getReceiverResolutionInfo()
                    .getPreferredRenderingResolution()
                    .equals(this.getReceiverResolutionInfo()
                            .getRenderingResolution());
            if (noChangeNeeded) {
//                debugOut(this.getCurrentResolution()+" is already preferred resolution of "+this.getReceiverResolutionInfo().getAvailableRenderingResolutions());
                return false;
            } else {
//                 debugOut(this.getCurrentResolution()
//                         +" => "+this.getReceiverResolutionInfo().getPreferredRenderingResolution()+" of "+this.getReceiverResolutionInfo().getAvailableRenderingResolutions());
                this.setReceiverResolution(this.getReceiverResolutionInfo()
                        .getPreferredRenderingResolution());
                return true;
            }
        }
        /* END HME 1.4.1e only */
        return false;
    }
 
    /** Multiplier relative to SD height of 480 */
    public double getHeightMultiplier() {
        if (isResolutionSupported()) {
            return getCurrentResolution().getHeight() / 480.0;
        } else {
            return 1.0;
        }
    }
 
    /** Multiplier relative to SD width of 640 */
    public double getWidthMultiplier() {
        if (isResolutionSupported()) {
            return getCurrentResolution().getWidth() / 640.0;
        } else {
            return 1.0;
        }
    }
 
}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License