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: 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 ...