Saturday, October 18, 2014

BlackBerry 10: Multiple OS versions from one source tree

Since launch, we've been iterating through several major versions of the BlackBerry 10 OS (10.0, 10.1, 10.2, 10.2.1, 10.3, and soon 10.3.1). Given that many users can't or don't upgrade immediately, and some never upgrade at all, this means we now have a wide variety in the field. While developers often want to provide quality support for the latest OS version, they also don't want to cut off support for older ones.

To keep the latest version of your app available across the full range of OS versions that users are running, it takes a few tricks. I figured it was about time to start writing up some of the tricks I've used to manage this.



Tricks used from QML

Abusing the Static Asset Selector

In OS 10.1, they introduced a feature to make it easier to support multiple device types from QML. This feature, the Static Asset Selector, works by designating a series of special directory names that are automatically selected based on the device. Originally, these directory names were based primarily on the device's screen resolution.

In OS 10.3, they expanded the feature set of the Static Asset Selector to support selection based on display density and pixel size minimums.

On top of all this, different devices were launched with different OS versions as their minimum requirement.

What this all means, is that you can actually use the Static Asset Selector to select different QML files or graphic assets based on the device OS version where certain screen resolutions and/or static asset selector features were introduced. This has limits, but it can still be quite useful.

For example:

Asset OS Version Devices
Foo.qml 10.0 Z10
720x720/Foo.qml 10.1 Q10, Q5, Classic
768x1280/Foo.qml 10.1 Z10
720x1280/Foo.qml 10.2 Z30, Z3
1440x1440/Foo.qml 10.3 Passport
mindw0h0du/Foo.qml 10.3 All
8ppd/Foo.qml 10.3 Z30, Classic
9ppd/Foo.qml 10.3 Q10, Q5
10ppd/Foo.qml 10.3 Z10
12ppd/Foo.qml 10.3 Passport

Wrapping UIConfig

In OS 10.3, they introduced a new UIConfig class that provides access to some useful constants and functions for UI design/layout. Specifically, they added a "design units" utility function and access to the current color palette. Chances are that you want to use both, whenever possible, regardless of what OS your app is actually running on. By leveraging the static asset selector and using the occasional constant, this can be done.

Display constants

For everything pre-10.3, you need to define some constants in QML. Since there's a direct 1:1 relationship between design units and screen resolution on devices running these older OSes, a simple resolution-based static asset selector approach can work.

In addition to containing the design unit scaling factors for each device type, these files also set the standard background color. The values used are based on a BlackBerry Developer blog post,  and take into account different color rules for dark-vs-bright and LCD-vs-OLED.

Setup code:
qmlRegisterType<bb::device::DisplayInfo>("bb.device", 1, 0, "DisplayInfo");
qmlRegisterUncreatableType<bb::device::DisplayTechnology>("bb.device", 1, 0, "DisplayTechnology", "");
Put this setup code in the constructor for your app if you want to be able to select different UI colors depending on display type. If you don't include it, then omit the DisplayInfo and DisplayTechnology sections from the 720x720 and 720x1280 QML files.


PageConstants.qml
import bb.cascades 1.0

QtObject {
    id: pageConstants
    property variant backgroundColor
    property real scalingFactor: 10.0
    onCreationCompleted: {
        if(Application.themeSupport.theme.colorTheme.style == VisualStyle.Bright) {
            backgroundColor = Color.create("#f8f8f8")
        } else {
            backgroundColor = Color.create("#121212")
        }
    }
}

720x720/PageConstants.qml
import bb.cascades 1.0
import bb.device 1.0

QtObject {
    id: pageConstants
    property real scalingFactor: 9.0
    onCreationCompleted: {
        if(Application.themeSupport.theme.colorTheme.style == VisualStyle.Bright) {
            var displayInfo = Qt.createQmlObject('import bb.device 1.0; DisplayInfo { }', pageConstants, 'dynamicDisplay')
            if(displayInfo.displayTechnology == DisplayTechnology.Oled) {
                backgroundColor = Color.create("#f2f2f2")
            } else {
                backgroundColor = Color.create("#f8f8f8")
            }
            displayInfo.destroy()
        } else {
            backgroundColor = Color.create("#121212")
        }
    }
}

720x1280/PageConstants.qml
import bb.cascades 1.0
import bb.device 1.0

QtObject {
    id: pageConstants
    property real scalingFactor: 8.0
    onCreationCompleted: {
        if(Application.themeSupport.theme.colorTheme.style == VisualStyle.Bright) {
            var displayInfo = Qt.createQmlObject('import bb.device 1.0; DisplayInfo { }', pageConstants, 'dynamicDisplay')
            if(displayInfo.displayTechnology == DisplayTechnology.Oled) {
                backgroundColor = Color.create("#f2f2f2")
            } else {
                backgroundColor = Color.create("#f8f8f8")
            }
            displayInfo.destroy()
        } else {
            backgroundColor = Color.create("#121212")
        }
    }
}

Utility containers

To actually use these constants as intended, and to take into account the fact that we don't need them for 10.3+, we need to define a utility QML file to reference them. This file has to inherit Container, rather than QtObject, since on 10.3 it needs to access objects that are only made available to UI controls.

UIUtility.qml
import bb.cascades 1.0

Container {
    property variant backgroundColor: pageConstants.backgroundColor
    function scale(propertyValue) {
        return Math.round(pageConstants.scalingFactor * propertyValue)
    }
    attachedObjects: [
        PageConstants {
            id: pageConstants
        }
    ]
}

mindw0h0du/UIUtility.qml
import bb.cascades 1.3

Container {
    property variant backgroundColor: ui.palette.background
    function scale(propertyValue) {
        return ui.sdu(propertyValue)
    }
}

Bringing it together

Here's a very basic example of a QML-implemented Page that uses these to set its background and padding:

import bb.cascades 1.0

Page {
    content: Container {
        background: uiutil.backgroundColor
        layoutProperties: StackLayoutProperties {
            spaceQuota: 1
        }
        topPadding: uiutil.scale(2)
        bottomPadding: uiutil.scale(2)
        leftPadding: uiutil.scale(3)
        rightPadding: uiutil.scale(3)
        Label {
            text: "Hello World"
        }
    }
    attachedObjects: [
        UIUtility {
            id: uiutil
        }
    ]
}

Tricks used from C++

Compile-time version check

If you plan to have separate builds of your application for each supported OS version, then this approach is probably the easiest. It basically uses preprocessor macros to select different blocks of code depending on what OS API target you're compiling with.

#include <bbndk.h>

class ApplicationUI : public QObject
{
    Q_OBJECT
public:
    ApplicationUI(QObject *parent=0);
    virtual ~ApplicationUI();

    void initialize();

#if BBNDK_VERSION_AT_LEAST(10,1,0)
    void initKeyListeners();
#endif

#if BBNDK_VERSION_AT_LEAST(10,2,0)
    void initNotificationSettings();
#endif

private slots:
    void onStartupComplete();
#if BBNDK_VERSION_CURRENT_MAJOR >= 10 && BBNDK_VERSION_CURRENT_MINOR >= 1
    void onKeyPressed();
#endif

#if BBNDK_VERSION_CURRENT_MAJOR >= 10 && BBNDK_VERSION_CURRENT_MINOR >= 2
    void onNotificationSettingsChanged();
#endif
};

This example uses the macros provided in "bbndk.h" two different ways. It uses the simpler version when selecting code that only the C++ compiler needs to actually pay attention to. It then uses a slightly more complicated (but actually simpler to preprocess) version on code that the Qt meta-object compiler (moc) needs to pay attention to. This is because moc doesn't have preprocessor handling that is quite as sophistocated as the compiler itself.

One caveat is that "bbndk.h" was only included in the SDK with 10.1. So if you need to build for 10.0, you'll need to copy this file into the appropriate location within your build tools and edit it accordingly.

Run-time version check

Run-time version checks can be useful when you don't want to have a different build to select behavior, or if you need to select based on version data too specific for a separate build. (For example, workarounds for bugs or fixes in patch releases.)

The easiest way to implement this sort of check is to have a function that mimics the behavior of those macros, and place it in some common namespace:

util.h
#ifndef UTIL_H
#define UTIL_H
namespace util
{
bool checkOSVersion(int major, int minor, int patch = 0, int build = 0);
};
#endif // UTIL_H


util.cpp
#include "util.h"

#include <QtCore/QString>
#include <QtCore/QList>
#include <bb/platform/PlatformInfo>

namespace util
{

bool checkOSVersion(int major, int minor, int patch, int build)
{
    bb::platform::PlatformInfo platformInfo;
    const QString osVersion = platformInfo.osVersion();
    const QStringList parts = osVersion.split('.');
    const int partCount = parts.size();

    if(partCount < 4) {
        // Invalid OS version format, assume check failed
        return false;
    }

    // Compare the base OS version using the same method as the macros
    // in bbndk.h, which are duplicated here for maximum compatibility.
    int platformEncoded = (((parts[0].toInt())*1000000)+((parts[1].toInt())*1000)+(parts[2].toInt()));
    int checkEncoded = (((major)*1000000)+((minor)*1000)+(patch));

    if(platformEncoded < checkEncoded) {
        return false;
    }
    else if(platformEncoded > checkEncoded) {
        return true;
    }
    else {
        // The platform and check OS versions are equal, so compare the build version
        int platformBuild = parts[3].toInt();
        return platformBuild >= build;
    }
}

};

Once you have this all in place, selecting on OS version is as easy as:

// do stuff

if(util::checkOSVersion(10,3)) {
    initWithNewVisualStyle();
}

// do more stuff

if(util::checkOSVersion(10,2,1,1925)) {
    // assume bug has been fixed
}
else {
    // do workaround for bug
}

Qt meta objects

Many Qt objects expose methods and properties that can be called dynamically. This is normally to enable them to be called from QML or used with signals/slots, but you can also leverage this from C++. Specifically, look for methods declated in an object's headers as either a slot or as Q_INVOKABLE. Likewise, for properties, look for Q_PROPERTY declarations.

One big advantage of these annotations is that they're visible at runtime. This means that you can, for example, build code against 10.2 that calls methods and properties not available until 10.3.

Properties

In 10.3, the Page class has the following property defined:
Q_PROPERTY(bb::cascades::ActionBarFollowKeyboardPolicy::Type actionBarFollowKeyboardPolicy READ actionBarFollowKeyboardPolicy
           WRITE setActionBarFollowKeyboardPolicy RESET resetActionBarFollowKeyboardPolicy NOTIFY actionBarFollowKeyboardPolicyChanged
           REVISION 3 FINAL)


We can set this property from an app built against the 10.2 API as follows:
page->setProperty("actionBarFollowKeyboardPolicy", 2);

Note: We need to use the numerical value of the enum, since we don't have the enum in the 10.2 API either, but this approach does work.

Methods

In 10.3, the following method was added to the ThemeSupport class:
Q_INVOKABLE void setVisualStyleAndPrimaryColor(bb::cascades::VisualStyle::Type visualStyle,
                                               const bb::cascades::Color& primary,
                                               const bb::cascades::Color& primaryBase = bb::cascades::Color());


This method lets you set the visual style (i.e. bright or dark) and the accent colors at runtime. If you cannot set these in your bar-descriptor.xml, due to them being user-configurable, calling this method at startup is far more reliable than setting environment variables (which worked fine in 10.2).

Here's how to call it from an app that was built against the 10.2 API:

bb::cascades::Application *app =
    bb::cascades::Application::instance();
bb::cascades::ThemeSupport *themeSupport =
    app->themeSupport();

bb::cascades::VisualStyle::Type visualStyle =
    bb::cascades::VisualStyle::Bright;
bb::cascades::Color primaryColor =
    bb::cascades::Color::fromARGB(0xFFFF3333);

QMetaObject::invokeMethod(themeSupport, "setVisualStyleAndPrimaryColor",
    Q_ARG(bb::cascades::VisualStyle::Type, visualStyle),
    Q_ARG(bb::cascades::Color, primaryColor));

Dynamic function binding

This is the most complicated of the approaches you can use, especially with C++, and thus should only be used if none of the above techniques are viable for your specific situation. It involves binding function pointers to the library functions you want to call, at runtime.

For this example, let's assume we want to write code that needs to disable navigation focus highlights on a Control. The APIs for this were introduced in 10.3.1 to deal with the Classic, but we want our code to be built against 10.2.0 or 10.3.0.

If we were building against 10.3.1, the code to do this would look something like this:
Control *myControl = page->findChild<Control *>("myControl");
Navigation *nav = myControl->navigation();
nav->setDefaultHighlightEnabled(false);

This code calls a function on the Control object that doesn't exist until OS 10.3.1, which returns an object. It then calls a member function of that object. So we basically have two functions that are new to the 10.3.1 API that we need to call.

First, we need to find the function names we want to call.  To do that, we'll need to run the "ntoarm-nm" utility included with the NDK on libbbcascades.so.
$ source /opt/bbndk/bbndk-env_10_3_1_632.sh
$ cd /opt/bbndk/target_10_3_1_632/qnx6/armle-v7/usr/lib
$ ntoarm-nm -D -C libbbcascades.so | grep Control | grep navigation
001c5ed4 T bb::cascades::Control::navigation() const
$ ntoarm-nm -D -C libbbcascades.so | grep setDefaultHighlightEnabled
001a945c T bb::cascades::Navigation::setDefaultHighlightEnabled(bool)
001a9470 T bb::cascades::Navigation::resetDefaultHighlightEnabled()

So we can see that the functions are there in the library. Great! Of course we actually need the compiler-mangled versions of the function names, which is why this looks a little scarier with C++ than with C:
$ ntoarm-nm -D libbbcascades.so | grep Control | grep navigation
001c5ed4 T _ZNK2bb8cascades7Control10navigationEv
$ ntoarm-nm -D libbbcascades.so | grep setDefaultHighlightEnabled
001a945c T _ZN2bb8cascades10Navigation26setDefaultHighlightEnabledEb
001a9470 T _ZN2bb8cascades10Navigation28resetDefaultHighlightEnabledEv


The next step is to create function pointers that match the signatures of those functions, and to use the dlsym() function to bind those pointers. At this point, I'll cut to a complete example that uses this approach to accomplish the same thing as that C++ code that can directly call the APIs:

#include <bb/cascades/Control%gt;

#include <dlfcn.h>

using namespace bb::cascades;

void MyClass::setControlDefaultHightlightEnabled(Control *control, bool enabled)
{
    void *navigationPtr;

    void *(*control_navigation)(Control *) = (void *(*)(Control *))dlsym(RTLD_DEFAULT, "_ZNK2bb8cascades7Control10navigationEv");
    if(!control_navigation) {
        qWarning() << "dlsym failed (navigation):" << dlerror();
        return;
    }

    navigationPtr = control_navigation(control);
    if(!navigationPtr) {
        qWarning() << "couldn't get pointer to Navigation object";
        return;
    }

    void (*navigation_setDefaultHighlightEnabled)(void *, bool) = (void (*)(void *, bool))dlsym(RTLD_DEFAULT,
        "_ZN2bb8cascades10Navigation26setDefaultHighlightEnabledEb");
    if(!navigation_setDefaultHighlightEnabled) {
        qDebug() << "dlsym failed (setDefaultHighlightEnabled):" << dlerror();
        return;
    }

    navigation_setDefaultHighlightEnabled(navigationPtr, enabled);
}

Conclusion

Actively supporting legacy OS versions is a necessary evil of development, since a significant percentage of users never upgrade their OS. Likewise, actively supporting the latest and greatest will keep your most enthusiastic users happy, since a significant percentage of them will upgrade as soon as possible.

Hopefully I've provided a useful collection of tricks towards achieving the goal of supporting your app on as many in-market OS versions as possible. Pretty much all of these tricks, except the compile-time preprocessor directives, can also be used entirely at runtime without needing separate builds of your app.

Good luck!

1 comment:

Đậu Đức said...

No link github?