Wednesday, August 19, 2020

Qt & Android: Create a 2D Game using QML

Description

This tutorial describes how to easily create a Qt app for Android using an open source physics simulator engine.

The Github project Box2D is a feature rich 2D rigid body physics engine, written in C++ by Erin Catto. It has been used in many games.  The Github project qml-box2d was created by Thorbjørn Lindeijer.  He states the goal of the qml-box2d plugin is to expose the functionality of Box2D (C++) as a QML plugin in order to make it easier to write physics based software in QML by using simple QML commands to create Box2D bodies, joints, sensors etc.

Source code

This post is going to describe  how to get started with the QML plugin qml-box2d in just a few easy steps.  The complete sample app is found in Github at https://github.com/pgulotta/TestBox2D


Step 1

Clone qml-box2d from Github, https://github.com/qml-box2d/qml-box2d.   The project's README.md provides instructions on how to install the Qt plugin. Take a look  at the README.md to learn how robust BOX2D is. Note, for this example, there is no need to install the Qt plugin because the qml-box2d plugin will be statically linked to this sample project.  

Step 2

Create a new Qt Quick empty project named TestBox2D at the same directory level as the cloned qml-box2d Github project.   To the project file TestBox2D.pro, add a reference to the static library:
INCLUDEPATH += ../qml-box2d/Box2D
include(../qml-box2d/box2d-static.pri)

Build the TestBox2D for both Desktop and Android kits: armeabi-v7a. arm64-v8a. x86. x86_64.  Verify the apps run successfully.


Step 3

Since the qml-box2d Github project provides many QML examples, we will use one of the examples.  Copy the files and the images directory from qml-box2d/examples/mouse into the TestBox2D project directory. The file main.qml is overwritten. Next, copy the qml-box2d/examples/shared directory into TestBox2D project directory.  Update the file qml.qrc to reflect the additional files.  The contents of qml.qrc will look like:
<RCC>
    <qresource prefix="/">

        <file>main.qml</file>
        <file>Wall.qml</file>

        <file>shared/BoxBody.qml</file>
        <file>shared/ChainBody.qml</file> 
        <file>shared/CircleBody.qml</file>
        <file>shared/EdgeBody.qml</file>
        <file>shared/ImageBoxBody.qml</file>
        <file>shared/PhysicsItem.qml</file>
        <file>shared/PolygonBody.qml</file>
        <file>shared/RectangleBoxBody.qml</file>
        <file>shared/ScreenBoundaries.qml</file>

        <file>images/wall.jpg</file>

    </qresource>
</RCC>

Step 4

Since the TestBox2D is statically linked, all QML files must import Box2DStatic 2.0 and not Box2D 2.0.  So every QML file, including those in the shared directory, should have an entry such as:

//import Box2D 2.0
import Box2DStatic 2.0

For this project, the import statements in files main.qml and Wall.qml need to updated:

//import "../shared"
import "shared"  

Finally, in main.qml, change the Rectangle type to Window, visible: true,  and add the import of QtQuick.Window. Resulting in something like:

import QtQuick 2.2
//import Box2D 2.0
import Box2DStatic 2.0
import "shared"
import QtQuick.Window 2.15
Window {
visible:true
width: 800
height: 600
property Body pressedBody: null
... 

Step 5

To make the qml-box2d types available to the TestBox2D app, register the Box2DStatic plugin in main.cpp by adding a couple of lines of code resulting in:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <box2dplugin.h>

int main( int argc, char* argv[] )
{
  QCoreApplication::setAttribute( Qt::AA_EnableHighDpiScaling );
  QGuiApplication app( argc, argv );

  Box2DPlugin plugin;
  plugin.registerTypes( "Box2DStatic" );

  QQmlApplicationEngine engine;
  const QUrl url( QStringLiteral( "qrc:/main.qml" ) );
  QObject::connect( &engine, &QQmlApplicationEngine::objectCreated,
  &app, [url]( QObject * obj, const QUrl & objUrl ) {
    if ( !obj && url == objUrl ) {
      QCoreApplication::exit( -1 );
    }
  }, Qt::QueuedConnection );
  engine.load( url );
  return app.exec();
}


Step 6

Clean, build, and run the TestBox2D app on both for both the desktop and Android platforms (or any other Qt supported platform).  Try out the app by tossing and stacking the different sized boxes (rectangles) around.  Note how the boxes appear to have gravity, friction, and restitution.  

You can try out other examples by following the steps described above or by using the Qt qmlscene tool. Pretty darn awesome, right!

Sunday, July 12, 2020

Qt & Android: Job Scheduler

Description

This tutorial describes how to schedule tasks in a Qt app for Android using Android's JobScheduler API. If your Android app needs to perform a repetitive task whether the app is active or not, you should consider creating an Android app with a JobService to perform the work.  You can add one or more job services, where each one defines a different task that is performed in the future based upon specified conditions.  The tasks can even survive application restarts and device reboots.


Source code

The following highlights the steps needed to create an Android app using Qt and Android JobScheduling API.  The complete sample app is found in Github at https://github.com/pgulotta/JobServiceExample.

Getting Started 

Create a new Qt Quick empty project naming it JobServiceExample.  The app will link against the Qt Android Extras module, so add this line to the project file:  

    android:QT += androidextras

 

This sample app displays a message box, so you need to add entry to support the QMessageBox class. 

    QT += widgets 


Create Android Package Templates files. This can be easily done with QtCreator. If you are creating the Templates manually and you need assistance, refer to the Qt for Android  documentation. 


Verify the app was generated correctly by building all Android ABI's and deploying your new app onto an  Android device or emulator.  Upon successfully running your new Android app, it's time to make the changes needed to create a job scheduling service. 

 

Update AndroidManifest.xml

In the AndroidManifest.xml, name the package following the normal java package naming conventions. The JobServiceExample package name is com.example.jobserviceexample. The AndroidManifest.xml file should contain an entry similar to this.

    <manifest package="com.example.jobserviceexample"  

        xmlns:android="http://schemas.android.com/apk/res/android" 

        android:versionName="100" android:versionCode="100" android:installLocation="auto">


There are several other changes to make to the AndroidManifest.xml and now is as good a time as any.  The job scheduling api requires a minimum android version.   Specify the supported Android sdk, with an entry such as this.

    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="29"/>


In the AndroidManifest.xml file , the JobServiceExample must be declared as a service with the BIND_JOB_SERVICE permission.  The name JobServiceExample is the name of the Java class you will be creating.

    <service android:name=".JobServiceExample" android:label="Example job service"

        android:permission="android.permission.BIND_JOB_SERVICE"/>


Because Android will run the job, per the job's defined schedule, after the system has finished booting, the RECEIVE_BOOT_COMPLETED permission is needed.

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>


The sample app scheduled task is to append a line of text composed of a time stamp to a file periodically.  Thus, the permission WRITE_EXTERNAL_STORAGE is required.

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>


Please refer to the sample app, if you are unsure as to what the AndroidManifest.xml entries are.


JobScheduler Java API

An Android service is a component that runs in background and has no user interface. The service will continue to run even if the application exits.  The work or task you are scheduling is known as a 'job' and is defined in a Java class which enables Android to perform the work, even when the app is not active. Refer to Android documentation for details. 


Java files belong to the package specified in the AndroidManifest.xml file.  Java files called by Qt application must be placed in a directory which conforms to the path hierarchy defined by the package name.  Create a java class called JobServiceExample.class which extends android.app.job.JobService in the directory ...JobServiceExample/android/src/com/example/jobserviceexample. This Java class is an Android Service that extends the Android JobService class.   Since JobServiceExample class extends the JobService class, a couple of methods must be implemented: onStartJob(), which is called by the system when the job has begun executing, and onStopJob(), which is called by the system if the job is cancelled before finishing. Note, JobServiceExample class runs on the main thread so the task should be run on a  worker thread. 


    @Override

    public boolean onStartJob( JobParameters jobParameters )

   {

        Log.i( TAG, "JobServiceExample.onStartJob : jobParameters.getJobId() = " +

            jobParameters.getJobId() );

        try {

            Thread thread = new Thread( new ExampleWork( this ) );

            thread.start();

            thread.join();

        } catch ( Exception e ) {

             e.printStackTrace();

        }

        return false;  // Returns false from when job has finished. onStopJob will not be invoked

    }


    @Override

    public boolean onStopJob( JobParameters jobParameters )

    {

         // This method is typically not invoked

         Log.i( TAG, "JobServiceExample.onStopJob : jobParameters.getJobId() = " +

            jobParameters.getJobId() );

         return false;  // Returns false to end the job entirely

    }



The class ExampleWork specified above implements Runnable.  For this example, the task is to append a line of text composed of a timestamp to a file.


    @Override

     public void run()

     {

        ...

       doWork( mContext );

        ...

     }

 

public static void doWork( Context context )

 {

   try {

       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

       String textToAppend = dateFormat.format(new Date());

       File path = Environment.getExternalStoragePublicDirectory( 

           Environment.DIRECTORY_DOWNLOADS );

       String filename =  path.getAbsolutePath() + File.separatorChar + 

       "JobServiceExampleLog.txt";

       Log.i( TAG, "ExampleWork.doWork path =" + filename + " appending text " + textToAppend);

       BufferedWriter writer = new BufferedWriter(new FileWriter(filename, true));

       writer.newLine();

       writer.write(textToAppend);

       writer.close();

   } catch ( IOException e ) {

       e.printStackTrace();

   }

 }


Schedule a job by using Android's JobScheduler java class. Using this class, Android can efficiently batch your job with other jobs that need network access. JobScheduler can request retries and when needed Android will rescheduled the work; the work is guaranteed.  In the sample project JobServiceExample, the class ExampleJobScheduler illustrates scheduling.  A unit of work is encapsulated by a java JobInfo class specifying the scheduling criteria. The JobInfo.Builder class is used to configure how the scheduled task should run.


    public static void scheduleJob (Context context, int intervalinMS )

   {

        handleJob(context, intervalinMS );

   }

    private static void handleJob (Context context, long intervalinMS)

    {

         ...

         ComponentName serviceComponent = 

         new ComponentName( context, JobServiceExample.class );

         ...

         JobScheduler jobScheduler = context.getSystemService( JobScheduler.class );

         ...

         JobInfo.Builder builder = new JobInfo.Builder( JOB_ID, serviceComponent );

         ...

         builder.setPeriodic( intervalinMS );  // job runs within the intervalinMS; API 21

         builder.setPersisted( true ); // persist this job across device reboots; API 21

         builder.setRequiredNetworkType( JobInfo.NETWORK_TYPE_ANY ); //  API 21

         builder.setRequiresDeviceIdle(false); //  API 21

         int result = jobScheduler.schedule( builder.build() );

         String resultText = ( JobScheduler.RESULT_SUCCESS == result ) ? 

        "successfully" : "failed";

        Log.i ( TAG, "ExampleJobScheduler.handleJob scheduled for intervalinMS of " + 

        intervalinMS + " is "  + resultText );

      ...

  }



QML

In this example app, the QML UI, main.qml,  allows the user to schedule how frequently the task is executed.

  ...

  Button {

    text: qsTr("Apply")

    anchors.horizontalCenter: parent.horizontalCenter

    onClicked: Controller.scheduleJobService(scheduleModelId.

        get(scheduleSelectorId.currentIndex).intervalMS)

    }

    ...



Qt & C++

The FrontController C++ class  exposes the job  scheduling function to the QML interface.


Q_INVOKABLE void scheduleJobService(int intervalinMS);

 ...

    void FrontController::scheduleJobService( int intervalinMS )

    {

         QAndroidJniObject::callStaticMethod<void>

         ( "com/example/jobserviceexample/JobServiceExample","scheduleJobService",

             "(Landroid/content/Context;I)V",

             QtAndroid::androidActivity().object(), intervalinMS);

    }


The Permissions C++ class is called when the application starts for check for and request of needed permissions.

    void Permissions::requestExternalStoragePermission()

    {

        ...

        QtAndroid::PermissionResult request = QtAndroid::checkPermission( 

            "android.permission.WRITE_EXTERNAL_STORAGE" );

        if ( request == QtAndroid::PermissionResult::Denied ) {

            QtAndroid::requestPermissionsSync( QStringList() <<  

            "android.permission.WRITE_EXTERNAL_STORAGE" );

             request = QtAndroid::checkPermission( 

             "android.permission.WRITE_EXTERNAL_STORAGE" );

             if ( request == QtAndroid::PermissionResult::Denied ) {

                 mPermissionGranted = false;

                 if ( QtAndroid::shouldShowRequestPermissionRationale( 

                 "android.permission.READ_EXTERNAL_STORAGE" ) ) {

                      QAndroidJniObject 

                      ( "com/example/jobserviceexample/ShowPermissionRationale",

                      "(Landroid/app/Activity;)V",

                      QtAndroid::androidActivity().object<jobject>());

                      QAndroidJniEnvironment env;

                      if ( env->ExceptionCheck() ) {

                          env->ExceptionClear();

              }

        }

    } else {

      mPermissionGranted = true;

    }

  } else {

    mPermissionGranted = true;

  }

  ...

}


The communication between the C++ Qt/QML and Java needs to be specified  in main.cpp.

    #include <QQmlContext>

    #include "frontcontroller.h"

    #include "permissions.hpp"


    int main(int argc, char *argv[])

    {

     ...

     QQmlApplicationEngine engine;

     FrontController frontController{app.parent()};

     engine.rootContext()->setContextProperty( "Controller", &frontController );

     const QUrl url(QStringLiteral("qrc:/main.qml"));

     QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,

       &app, [url](QObject *obj, const QUrl &objUrl) {

           if (!obj && url == objUrl)

               QCoreApplication::exit(-1);

        }, Qt::QueuedConnection); engine.load(url);


      Permissions permissions;

      permissions.requestExternalStoragePermission();

      if ( permissions.getPermissionResult() ) {

          qInfo( "Successfully obtained required permissions, app starting" );

          return app.exec();

      } else {

          qWarning( "Failed to obtain required permissions, app terminating" );

      }

  }


Test it Out

Upon successfully building the app, run the JobServiceExample app and schedule a job by selecting the "Recording interval" and pressing "Apply".  Quit the app.  On Android, the file /storage/emulated/0/Download/JobServiceExampleLog.txt will be updated at the specified time interval.  Upon rebooting the Android, you can observe, the file /storage/emulated/0/Download/JobServiceExampleLog.txt will continue to be updated at the specified time interval.  The sample app, JobServiceExample, logs often to help you follow along.

        



Conclusion

To code a job scheduler in your QT Android application, there are many small steps, but this is true with all Android app development.  Android job scheduling  is a powerful, performant, robust feature that enables the Android OS to shoulder the burden of executing tasks based upon specific conditions. It's a nice tool to have in your toolbox.






Friday, May 1, 2020

Qt & Android: Setting wallpaper

Is your Android app written using Qt? Would you like to change the wallpaper on your Android device programmatically? Then join me in creating an Android app that generates and saves the image as wallpaper.

Using Qt Creator, create a new 'Qt Quick Application - Empty' naming it WallpaperExample.


Select both Desktop and Android Kits.  It is usually convenient to have a Desktop app to test non-Android features of the app.


Next, verify the WallpaperExample Desktop builds and runs.


Update main.qml so that it can used to set the wallpaper.


Create the AndroidManifest.xml. Additional changes to the XML will be neccessary later.
Select all the Android supported ABI's.


Now, verify the WallpaperExample Android builds and runs.


An effective  approach  for a Qt/QML/C++ app to perform Android specific tasks is to use Android's Java API. Android Java classes are accessible to a Qt application using the JNI convenience APIs in the Qt Android Extras module, such as QAndroidJniObject::callStaticObjectMethod.


Java classes in a Qt Android project need to maintain the Java naming conventions and the directory package structure.
The package name is the same package name assigned in the Android-Mainfest.xml.
Because this Java class, WallpaperGenerator,  requires access to application-specific resources, the Android Context must be passed in from QAndroidJniObject::callStaticMethod call in the Qt class.


The QAndroidJniObject::callStaticMethod calls the static method WallpaperGenerator.generateWallpaper which generates and sets the Android wallpaper on a seperate thread to prevent UI delay.










The method WallpaperGenerator.getWallpaper calls an image generating service and reads the response using common Java API. 
The method WallpaperGenerator.setWallpaper creates and sets the bitmap image using Android  specific API. 
An alternative to calling the methods getWallpaper and setWallpaper, the method generateWallpaper can be called.













AndroidManifest.xml requires a few changes before the app can run. Android requires the manifest.permission.SET_WALLPAPER permission. Starting with Android 9, API level 28, cleartext support is disabled by default and this app communicates with the wallpaper generating server using http, so usesCleartextTraffic must be enabled.


That's all it takes. Run the app and set wallpaper.


























Qt & Android: Create a 2D Game using QML

Description This tutorial describes how to easily create a Qt app for Android using an open source physics simulator engine. The Github proj...