I’ve been using this BlackBerry KeyOne android device since it was released in 2017. Before that I had tried the Priv (also android) for a few months but eventually went back to my older BB10 devices like the Classic and Passport. Now, it’s 2019. BlackBerry 10 is soon to be extinct, and I think it’s finally time that I try to port my Cascades apps to Android.
I start with the most simple BB10 app I made, Daily Wallpaper. This app automatically updates your phone wallpaper to Bing’s daily homepage image. Back when I first released it on BlackBerry World it was the only available app to provide this sort of functionality. Now, if you search the Google Play Store there are dozens of apps that do exactly this same thing.
So, I’m not doing the android ecosystem any favors by porting over a simple app that already exists in multitudes. However, if I can ever hope to port one of my more complex apps, for instance Reddit in Motion… then I have to start with something basic and see how it goes. Hopefully this series of posts will help any BB10 developers who are thinking about porting over their apps to Android, since I found out very quickly that there aren’t any online resources for this sort of thing. Going from Qt-based Cascades to Qt for Android is relatively easy, but there are plenty of differences between the platforms and here we will highlight the more difficult obstacles. Let’s begin.
Getting started
First thing is to download the Qt Creator IDE from their website. This is a big install (~35gb) since the Qt library is so huge.
Next we need to configure our environment for working with Android. Follow this guide from the Qt Docs about getting all the prerequisite tools installed and configured. The only hiccup I had during this part was with my existing Java install. I had to download a fresh copy of JDK v8 before things started working properly.
Making something functional
The goal for this first blog post is getting to a point where most things are simply functioning properly, then I will write a follow-up post where we polish the UI and round-out all the features. I think the easiest place to start working in the code is the UI with QML. Unfortunately, even with an extremely simple UI like my Daily Wallpaper app, we can’t just copy paste the qml code. The Cascades UI components we’re familiar with are all descendants of the Qt UI components so the properties and functions will be familiar, we just need to find each equivalent object. Below we can compare my original Cascades qml code (only the relevant components) with the new qml for Android
//Original Cascades QML file import bb.cascades 1.0 import bb.system 1.2 import bb.platform 1.2 import org.domisy 1.0 import bb.cascades.pickers 1.0 Page { id: page titleBar: TitleBar { title: "The Bing Image of the Day" } actions: [ ActionItem { title: "Set Wallpaper" imageSource: "asset:///images/setBackgroundIcon.png" ActionBar.placement: ActionBarPlacement.OnBar onTriggered: { app.setImageWallpaper(bingWebImage.url); } }, ActionItem { title: "Refresh" imageSource: "asset:///images/reloadIcon.png" ActionBar.placement: ActionBarPlacement.InOverflow onTriggered: { shareAction.enabled = false; infoAction.enabled = false; app.initiateRequest(); } }, ActionItem { title: "Info" id: infoAction enabled: false imageSource: "asset:///images/aboutIcon.png" ActionBar.placement: ActionBarPlacement.InOverflow onTriggered: { app.showImageInfoToast(); } } ] Container { layout: DockLayout { } ActivityIndicator { objectName: "indicator" id: indicator verticalAlignment: VerticalAlignment.Center horizontalAlignment: HorizontalAlignment.Center preferredWidth: 200 preferredHeight: 200 } WebImageView { // a custom UI component for loading images from URL objectName: "bingImageComponent" id: bingWebImage preferredHeight: maxHeight - 200 preferredWidth: maxWidth scalingMethod: ScalingMethod.AspectFill horizontalAlignment: HorizontalAlignment.Center verticalAlignment: VerticalAlignment.Center } } }
//NEW qml file for Android import QtQuick 2.8 import QtQuick.Controls 2.1 import QtQuick.Window 2.1 import QtQuick.Dialogs 1.2 ApplicationWindow { visible: true width: Screen.width height: Screen.height property alias imageDescription: infoDialog.text header: Label { text: qsTr("The Bing Image of the Day") font.pixelSize: Qt.application.font.pixelSize * 1.5 padding: 10 } Page1Form { id: page1 objectName: "page1" property alias indicator: indicator property alias bingImageComponent: bingImageComponent width: parent.width height: parent.height BusyIndicator { id: indicator objectName: "indicator" anchors.centerIn: parent running: true z: 100 } Image { id: bingImageComponent objectName: "bingImageComponent" fillMode: Image.PreserveAspectFit width: parent.width height: parent.height } } footer: ToolBar { id: tabBar ToolButton { text: qsTr("Refresh") icon.source: "images/reloadIcon.png" leftPadding: 10 onClicked: { page1.indicator.running = true; app.initiateRequest(); } } ToolButton { text: qsTr("Set Wallpaper") icon.source: "images/setBackgroundIcon.png" x: parent.width / 2 onClicked: { page1.indicator.running = true; app.setImageWallpaper(page1.bingImageComponent.source); } } ToolButton { id: infoButton objectName: "infoButton" text: qsTr("Info") icon.source: "images/ic_info.png" x: 70 onClicked: { infoDialog.open(); } } } MessageDialog { id: infoDialog title: "Image Description" text: qsTr("No info!") standardButtons: StandardButton.OK } }
Again, this new QML code doesn’t look great. We will need to do some extra work later to make everything look as pretty as it did with Cascades. But, it gives us just enough to work with the functionality of the app. I think that providing both versions of this code side by side is self-explanatory enough. You can see which components correspond to each other and the different names of properties and layout code.
On with the logic
A few quick things to add in our main.cpp file. Lines 10-13 give us access to our C++ functions in DailyWallpaper.cpp from the QML code. Also notice the small difference in the way the main.qml file is loaded, lines 6-9. That code was auto-generated when we created the new project, but then line 10 is useful for grabbing the root object to pass on as the parent QObject parameter of DailyWallpaper constructor. In Cascades land, that same parameter was of type bb::cascades::Application.
#include "dailywallpaper.h"
#include <QQmlContext>
int main(int argc, char *argv[])
{
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
QObject *rootObject = engine.rootObjects().first();
DailyWallpaper* dailyWallpaper = new DailyWallpaper(rootObject);
QQmlContext* context = engine.rootContext();
context->setContextProperty("app", dailyWallpaper);
return app.exec();
}
From here it’s a matter of carefully copying each function from DailyWallpaper.cpp into the new project. Thankfully most of this C++ code transfers very easily, however you don’t want to throw it all in right away since each piece will probably give you some things to deal with before moving on. Here are the two versions of the DailyWallpaper constructor, for comparison.
//Cascades DailyWallpaper.cpp
DailyWallpaper::DailyWallpaper(bb::cascades::Application *app) :
QObject(app)
{
QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
qml->setContextProperty("app", this);
AbstractPane *root = qml->createRootObject<AbstractPane>();
mFile = new QFile("data/bingWallpaper.jpg");
bingImage = root->findChild<WebImageView*>("bingImageComponent");
activity = root->findChild<ActivityIndicator*>("indicator");
mNetworkAccessManager = new QNetworkAccessManager(this);
bool result = connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), this,
SLOT(requestFinished(QNetworkReply*)));
Q_ASSERT(result);
Q_UNUSED(result);
initiateRequest();
app->setScene(root);
}
//Android DailyWallpaper.cpp
DailyWallpaper::DailyWallpaper(QObject *parent) : QObject(parent)
{
QString path = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
mFile = new QFile(path.append("/bingWallpaper.jpg"));
rootObject = parent;
indicatorObject = rootObject->findChild<QObject*>("page1")->findChild<QObject*>("indicator");
imageObject = rootObject->findChild<QObject*>("bingImageComponent");
mNetworkAccessManager = new QNetworkAccessManager(this);
bool result = connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), this,
SLOT(requestFinished(QNetworkReply*)));
Q_ASSERT(result);
Q_UNUSED(result);
initiateRequest();
}
We see that Cascades was loading up the main.qml file here in the DailyWallpaper constructor instead of from main.cpp. Because of this we establish the root QObject in a different way, and then we use it to grab references of certain important components in our QML file
I won’t go through ALL of the code so deliberately. From here let’s pick out the random trouble spots I found. Luckily all the network code using QNetworkAccessManager works a charm. The first issue comes up with how I was handling the QNetworkReply JSON response.
//Cascades DailyWallpaper.cpp
const QByteArray response(reply->readAll());
bb::data::JsonDataAccess jda;
QVariantMap results = jda.loadFromBuffer(response).toMap();
QVariantMap children = results["images"].toList().at(0).toMap();
imageUrl = "http://www.bing.com" + children["url"].toString();
imageInfo = children["copyright"].toString();
//Android DailyWallpaper.cpp
const QByteArray response(reply->readAll());
QJsonDocument jsonDoc = QJsonDocument::fromJson(response);
QVariantMap results = jsonDoc.object().toVariantMap();
//rest is the same
There’s not much to say here since it’s just a small change of syntax. We swap in QJsonDocument instead of using BlackBerry’s JsonDataAccess, and then we get our JSON data into a regular QVariantMap. That’s it.
Using Qt Android Extras to run Java code
Now that we have the network code working and we have the data we need, it’s time to write the code which will set the device wallpaper. This was the first real struggle. We are reminded that Qt is not the real native platform for Android, and that there are some things that can only be accessed via Android’s Java API’s. Android’s WallpaperManager is one of these services that we do not have the ability to control directly from Qt.
Luckily for us, Qt allows us to write and execute native Java code so that we essentially have access to all native API’s. It can be a little tricky at first to get it running though. Here is the documentation. There’s a nice sample application about using notifications to help us get started, but there are some unique things about the WallpaperManager API which this sample does not demonstrate.
With Cascades it was a matter of a few lines of simple C++ code. We could execute this right after downloading the image to the device.
//Cascades C++
bb::platform::HomeScreen homeScreen;
bool result = homeScreen.setWallpaper(QUrl("data/bingWallpaper.jpg"));
if (result)
//success
else
//failed
I will assume you read through the docs I linked above to have an understanding of Qt Android Extras. Now let’s just go through the process of accessing and using the WallpaperManager API for this scenario. We have the URL of the image we want to set as the wallpaper, so we use QNetworkAccessManager to download the image data and QFile to save the image to a local file on the device. Now we have a QString filePath to the location of the local file. I put this helper function updateAndroidWallpaper() into WallpaperControls.cpp where we call on the Java code with QAndroidJniObject.
//Android wallpapercontrols.cpp
#include "wallpapercontrols.h"
#include <QtAndroidExtras/QAndroidJniObject>
#include <QtAndroid>
WallpaperControls::WallpaperControls(QObject *parent)
: QObject(parent)
{}
void WallpaperControls::updateAndroidWallpaper(QString filePath) {
QString message = "Wallpaper set successfully";
QAndroidJniObject javaNotification = QAndroidJniObject::fromString(filePath);
jboolean wasSuccessful = QAndroidJniObject::callStaticMethod<jboolean>("com/domisy/wallpaper/WallpaperControls",
"setWallpaper",
"(Ljava/lang/String;Landroid/content/Context;)Z",
javaNotification.object<jstring>(), QtAndroid::androidContext().object());
if (!wasSuccessful)
QString message = "Something went wrong!";
}
Unlike the notifications example in the docs, you notice we are passing two parameters to our Java function. The first is the filePath to the image, and the second is the application Context. We need the context in order to obtain an instance of the WallpaperManager. And it’s important that we pass the Context as a parameter from our C++ side, because it isn’t accessible from the Java code. Context can only be obtained from the main application thread which, for us, is on our Qt side. Below is the Java class WallpaperControls which shows the function we’re calling from C++ called setWallpaper().
// Android WallpaperControls.java
package com.domisy.wallpaper;
import java.lang.Object;
import android.app.WallpaperManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.content.Context;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
public class WallpaperControls extends org.qtproject.qt5.android.bindings.QtActivity {
private static WallpaperControls m_instance;
public WallpaperControls()
{
m_instance = this;
}
public static boolean setWallpaper(String s, Context c)
{
WallpaperManager wallpaperManager = WallpaperManager.getInstance(c);
Bitmap b = null;
try {
FileInputStream is = new FileInputStream(new File(s));
BufferedInputStream bis = new BufferedInputStream(is);
b = BitmapFactory.decodeStream(bis);
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}
try {
wallpaperManager.setBitmap(b);
return true;
} catch (IOException ex) {
ex.printStackTrace();
return false;
}
}
}
One of the most tricky things about working with Java code from Qt is keeping in mind which things can be accessed from each side, and then figuring out how to use both together at the same time. Now we want to display a simple Toast notification to tell the user that the wallpaper was set successfully. Once again, we need to use Java since Qt doesn’t have support for Toast notifications. Right away it seems like an obvious solution to generate the Toast at the end of our setWallpaper() Java function, after we’ve use WallpaperManager to set the image. We’re already in the Java code after all, we can kill two birds with one stone, right?
NOPE. Just like WallpaperManager needed to run from the application Context, the Android Toast notifications need to be generated on the UI thread. Try to instantiate a Toast in the Java code and you quickly find that there’s no access to the UI thread there, at least not in our WallpaperControls class. (It should be noted that you can indeed run Toasts from Java with Qt Android Extras, as demonstrated in this blog post. However, we would need to create another Java class just for that Toast code, and there’s a much more easy way to do it, using an example from this other blog post!). We use QAndroidJniObject again, except this time instead of calling a Java function, we create a Toast object all from the C++ side.
void WallpaperControls::updateAndroidWallpaper(QString image) {
//............
//after our previous use of QAndroidJniObject to call our Java function setWallpaper()
Duration duration = LONG;
QtAndroid::runOnAndroidThread([message, duration] {
QAndroidJniObject javaString = QAndroidJniObject::fromString("Wallpaper set successfully!");
QAndroidJniObject toast = QAndroidJniObject::callStaticObjectMethod("android/widget/Toast", "makeText",
"(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;",
QtAndroid::androidActivity().object(),
javaString.object(),
jint(duration));
toast.callMethod<void>("show");
});
}
enum Duration {
SHORT = 0,
LONG = 1
};
This code is a little convoluted, but if you examine it carefully you see that we are generating the Toast object from the C++ code, passing in the two parameters it requires which are the string message and the duration. Then once we have a handle on our Toast object, we call the show() method to make it display. The whole time we stay in C++ code but manage to manipulate objects that are only accessible from the Java side. Neat!
Conclusion
Okay, I think that is more than enough for one post. Now the app is functional. At this point the app can load the image to view it, we can set the device wallpaper, and we can see the description of the image. And we got a Toast notification to make us feel better! Next up is implementing the auto-update feature, where the app can automatically update the wallpaper from the background each day without any user interaction. That will require creating an Android Service which sounds like a blast and a half. And also polishing up the UI with some QML work!
2 responses to “Porting a (relatively simple) BlackBerry Cascades app to Android with Qt – Part 1”
I have this Blackberry Torch 9810, when I insert SIM card (which is perfectly fine and inserted properly) in it, it seems to let me select the network but it still shows SOS on top instead of network bars. Whenever I select my carrier network it says Network Selected. Only emergency service is available. Can anyone help me with this?
Thanks man. This is great. I might port the app to BB 10 when I release the android app just to have a presence on BB and then take my time to rewrite the BB app natively or by using cascades. Michael Nana Aug 3 ’13 at 11:42 OP asked for code porting, not running the APK on BB10. And if that s (mainly) true that an APK converted to a BAR will run on BB10, it s clearly not the optimal solution. Marc Plano-Lesay Aug 4 ’13 at 12:30