We understand how alarms and background tasks work

CarderPlanet

Professional
Messages
2,549
Reaction score
746
Points
113

Wrench in touch!​

This article was written for educational purposes only. We do not call anyone to anything, only for information purposes! The author is not responsible for your actions.

Today we will learn how in OS Android you can create your own task scheduler beautifully and elegantly, which will be useful in your, of course, your bright affairs. To do this, we need only standard tools: the Android SDK from Google and the Eclipse code editor with the ADT plugin.

FORMULATION OF THE PROBLEM​

Our planner should be able to:
  • add an unlimited number of tasks at arbitrary times;
  • perform background tasks (even if the device falls asleep);
  • safely survive the device reboot.

TURN ON SIGNALING!​

To accomplish this task, Android provides a special Java class AlarmManager (alarm manager) that allows you to set an alarm ( Alarm) that is triggered at a specified time or with a specified frequency. The signaling date and time format is the good old UNIX-time, that is, the time expressed in milliseconds since January 1, 1970.

Before moving on, it is necessary to clarify the situation when, for example, we have hundreds or even thousands of tasks planned. It is quite obvious that installing the same number of alarms is not a good idea (hmm, a new vector towards DoS?). Instead, we will only use one signaling, which we will enable dynamically. Our scheduler will execute all tasks sequentially.

ARCHITECTURE OF THE PROJECT​

Our project will be divided into four modules (see source):
  1. Main activity (Main.java).
  2. Broadcast receiver (AlarmReceiver.java).
  3. Background service (AlarmService.java).
  4. Job database ( AlarmDB.java).

fig1.png

Getting Started

The main activity (Main.java), in fact, is the graphical interface of our application, consisting of standard components - text labels (TextView), input fields (EditView) and the main button (Button). Its main goal is to determine the parameters of the task: date and time, the frequency of operation, the task itself.

The direct installation of alarms, as well as all the useful functionality of our tasks, will be performed in the background service ( AlarmService.java). Since we have a good magazine, we will simply display a notification with sound as a combat load. The background service (class Service) in Android runs in the main thread of the application and requires the use of multithreading for long-term operations (otherwise you risk seeing the annoying ANR "Application not responding"). For our purposes, a class IntentService (inheritor Service) is perfect, which, firstly, will independently create a worker thread, and secondly, it will automatically terminate after the task is completed.

Database ( AlarmDB.java) is a helper class for putting jobs into SQLite database and retrieving them. The structure of the table is shown in the sidebar. It makes no sense to dwell on this module, since it uses standard SQL queries of the form REPLACE/SELECT.

You probably have a question: what is a broadcast receiver ( AlarmReceiver.java) for? The fact is that when the device is restarted, all alarms AlarmManager’а disappear, which, of course, does not suit us (see the last point of the problem statement). The only thing we can do is ask the system to run our code, which restores all tasks, immediately after boot (see the sidebar for the appropriate permission for the application). A procedure like this should be wrapped in a broadcast receiver ( BroadcastReceiver) and defined in ( public void onReceive). In our case, this function will be defined by static (static void scheduleAlarms), which will allow you to call it from anywhere in the application. Any broadcast receiver should work as quickly as possible - Android takes a maximum of ten seconds for this, otherwise the receiver will probably (depending on many factors - Android version, manufacturer's firmware features) be interrupted. This is enough time to start our background service.

Summarizing, let's draw up an algorithm for the application. After defining the task in the main activity, it is placed in the database, after which the static function scheduleAlarms from the class AlarmReceiver is called (it is also called when the device is loaded). The only thing this function does is to start a background service with a special key SET_ALARMthat notifies you of the need to retrieve the nearest job from the database and set it on alarm. As soon as it works, the background service starts again, but with a key RUN_ALARMthat executes the exploit (just kidding, displays a notification) and calls again scheduleAlarms, but already to set the next alarm, that is, the next task, and so on. Thus, we actually use only one signaling for an unlimited number of tasks.

This is where the theory ends and, oddly enough, coding begins.

Database structure​

For simplicity, our database will consist of a single table:

Code:
CREATE TABLE tbSheduler (

    _id INTEGER PRIMARY KEY AUTOINCREMENT,

    utime NUMERIC,

    msg TEXT ),

where _id is the primary key, utime is the scheduled time of the task, and msg is the message text. In a combat project, instead of a type field, TEXT it would be more correct to specify a unique key ( FOREIGN KEY) describing the task within the framework of other related tables.

HOME ACTIVITY​

For the interface of the main activity located in the file Main.java, we will select three input fields: edDate - to define the date and time of the task triggering, edRepeat - to indicate the required number of days to repeat, edMessage - for the message text itself. In the method onCreate , using, findViewByIdwe get links to all components GUI, and for the button bGo we define a handler:

Code:
public void bGo_click(View v){

    Calendar c = getCalendarFromDate(edDate.getText().toString());

    int repeat = getIntFromString(edRepeat.getText().toString());

    String message = edMessage.getText().toString();

    // Проверки на корректность ввода пропущены

    AlarmDb db = new AlarmDb(this);

    for (int i = 0; i < repeat; i++){

       db.insertAlarm(c.getTimeInMillis(), message);

       c.add(Calendar.DAY_OF_MONTH, 1);

    }

    AlarmReceiver.scheduleAlarms(this);

}

Startup​

Permission to autoload the application must be requested in the manifest file ( AndroidManifest.xml):

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

When you publish your application in the Play Market, the good manners recommend in the application description to clearly explain to the user why, in fact, you need autoloading, otherwise the user may be frightened.

Here, the helper function getCalendarFromDate returns an instance of the class Calendar with the date set as a parameter. Pay attention to specifying the format of the date string: of String fmt = "dd.MM.yyyy HH:mm"course, you can define your own. The function getIntFromString corny converts a string to a number for a variable repeat.

GUI of the main activity of our application in the editor ...

... and on a smartphone
The following loop puts the task into the database using the insertAlarm ( long UTIME, String MSG) method , which takes UNIX-time and the message text as parameters. Then, using the ( с.add) method , the calendar instance is shifted forward one day. The entire loop continues repeat times. We use a constant Calendar.DAY_OF_MONTH for the addition of one day, but you can also use the constants Calendar.MONTH, Calendar.YEAR, Calendar.HOUR_OF_DAY, Calendar.MINUTE and the like. If the second parameter is negative, the specified parameter is subtracted.

At the end of the function, a static method is called scheduleAlarms(this), which is defined in the broadcast receiver AlarmReceiver and initializes the signaling setting. As a parameter, we pass a link to the current context. This method will have another form, but more on that later.

BROADCASTING RECEIVER​

The receiver code ( AlarmReceiver.java) is shown below:

Code:
public class AlarmReceiver extends BroadcastReceiver {

  @Override

  public void onReceive(Context context, Intent intent) {

     scheduleAlarms(context);

  }

  static void scheduleAlarms(Context ctxt) {

     long NOW = Calendar.getInstance().getTimeInMillis();

     startAlarmService(ctxt, NOW);

  }

  static void scheduleAlarms(Context ctxt, long TIME) {

     startAlarmService(ctxt, TIME);

  }

  static void startAlarmService(Context ctxt, long UTIME) {

     Intent i = new Intent(ctxt, AlarmService.class);

     i.setAction(AlarmService.SET_ALARM);

     i.putExtra("utime", UTIME);

     ctxt.startService(i);

  }

}

This code requires some clarification. The class AlarmReceiver inherits from the superclass BroadcastReceiver, which requires the implementation of an abstract method public void onReceive ( Context context, Intent intent) that is triggered when a certain intent is received (in our case - intent.action.BOOT_COMPLETED).

Any receiver must be necessarily registered in the application manifest ( AndroidManifest.xml):

Code:
<receiver android:name=".AlarmReceiver" >

 <intent-filter>

    <action android:name="android.intent.action.BOOT_COMPLETED" />

 </intent-filter>

</receiver>

The method scheduleAlarms has two forms: with the indication of time (extended), let's call it basic, relative to which the alarm should be set, and without. If the time is not specified, then the current time is used as the base one, that is, we will set alarms for the near future (ahead of the current one) and discard all alarms in the past. The extended version will come in handy later.

The culmination of the broadcast receiver will be a call to the method startAlarmServicethat starts the background service. StartService Intent is used as a parameter. Intent ( Intent) is the backbone of the mechanism for invoking various components in Android. These are activities, services, receivers, and so on. In our case, an instance of our service ( AlarmService) is specified as an intent. The class IntentService processes all intents in turn. To pass the string key, we will SET_ALARM use the method SetActionthat makes our intent unique in the system (by the way, for uniqueness, the SET_ALARM package name is included in the constant: SET_ALARM = "com.example.cron_SET_ALARM"). Also, intents can contain fields of the "key = value" type, which we will use, specifying the base time (method putExtra) as the utime key. Finally, we come to the most important part of our scheduler - the background service.

BACKGROUND SERVICE​

First, the background service needs to be registered in the application manifest:

Code:
<service android:name=".AlarmService" >

</service>

The service code can be conditionally divided into two parts: the installation of the alarm and the alarm itself. Let's start with the setup (to save valuable log space, debug print is omitted):

public class AlarmService extends IntentService {

@Override

protected void onHandleIntent(Intent intent) {

  Bundle extras = intent.getExtras();

  long TIME = extras.getLong("utime");

  if (intent.getAction().equalsIgnoreCase(SET_ALARM)) {

      AlarmDb db = new AlarmDb(this);

      db.open();

      Cursor c = db.select_NEXT_ALARM(TIME);

      AlarmManager mgr = (AlarmManager)this.getSystemService(Context.ALARM_SERVICE);

      Intent i = new Intent(this, AlarmService.class);

      if (c.getCount() > 0) {

           c.moveToFirst();

           int UTIMEi = c.getColumnIndex("utime");

           long UTIME = c.getLong(UTIMEi);

           i.putExtra("utime", UTIME);

           i.setAction(AlarmService.RUN_ALARM);

           PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);

           mgr.cancel(pi);

           mgr.set(AlarmManager.RTC_WAKEUP, UTIME, pi);

      } else {

           PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);

           mgr.cancel(pi);

      }

      c.close();

      db.close();

  }

}

The attentive reader will probably notice that the class AlarmManager already has a suitable function for setting periodic alarms - setRepeatingbut, nevertheless, we do not use it. This feature is certainly good if you only have a dozen alarms, but if there are ten times as many? Managing this whole zoo will be much more difficult than in our case. In addition, as you can see for yourself, manual calculation of the periodicity using the Java calendar ( Calendar) is not particularly difficult.

The service ( IntentService) handles the next incoming intent in the protected void onHandleIntent ( Intent intent) method. Having received the intent ( Intent), we retrieve the data that was packed earlier in the broadcast receiver - the base time ( getLong) and the flag ( getAction). If the flag matches a constant SET_ALARM, we connect to the database and get a record according to the generated in the db.select_NEXT_ALARM(TIME) request:

Code:
SELECT utime FROM tbSheduler WHERE utime >= TIME ORDER BY utime LIMIT 1

That is, we are requesting the closest task with a time ahead of the base one (which we extracted into a variable TIME using getLong). Of course, when defining the first task, it TIMEwill be the current time of the system.

Having received the task and having determined its time in a variable UTIME, we proceed directly to the installation of the alarm. As before, the broadcast receiver, we must create a plan associated with our back-end service: Intent i = new Intent(this, AlarmService.class)and indicate the alarm flag:) i.setAction(AlarmService.RUN_ALARMand trip time i.putExtra("utime", UTIME). Since our intent is deferred until firing time, it must be wrapped in a pending intent ( PendingIntent):

Code:
PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);

The flag FLAG_UPDATE_CURRENT means that if the intent has already been created, it is only necessary to update its data: in our case, the key-value utime (without this flag, the intent will not change).

So, we are ready to set up an alarm:

Code:
AlarmManager mgr = (AlarmManager)this.getSystemService(Context.ALARM_SERVICE);

mgr.set(AlarmManager.RTC_WAKEUP, UTIME, pi);

First, we access the alarm manager system service by specifying the appropriate constant - Context.ALARM_SERVICE. To set up an alarm, we will use a method set ( setRepeating see the sidebar for a method), passing it the alarm response time as the second parameter UTIME, and the pending intent we have already prepared as the third parameter pi. Application as the first parameter AlarmManager.RTC_WAKEUP leads to the fact that the alarm will wake up the device (see the second point of the problem statement). If we didn't need to wake up the device, then we could specify a flag AlarmManager.RTC to deliver the intent after the device wakes up.

Sleeping Android​

Despite the fact that we used the flag AlarmManager.RTC_WAKEUP, there is a non-zero chance that the background service ( IntentService) woken up with it will not actually have time to start processing intents. It's all about wake-up blocking, which the broadcast receiver ( BroadcastReceiver) and the signaling manager ( AlarmManager) have to wake up the device, but which is released after the code is executed in the onReceive receiver. The background service does not have such a lock. On the pages of "Hacker" this topic has already been raised in # 6 for 2013 (see "Tasks in interviews").

If there are no more matching jobs in the database, the signaling is canceled by a method cancel(pi)that takes the same pending intent as a parameter.

fig4.png

Our planner at work

It remains for us to consider only the alarm code. Here, as you will see, everything is trivial:

Code:
long TIME = extras.getLong("utime");

...

if (intent.getAction().equalsIgnoreCase(RUN_ALARM)) {

    AlarmDb db = new AlarmDb(this);

    db.open();

    Cursor c = db.select_ALARM_BY_UTIME(TIME);

    int MSGi = c.getColumnIndex("msg");

    String title, text;

    c.moveToFirst();

    if (!c.isAfterLast()) {

        title = "Тревога!";

        text = c.getString(MSGi);

        String link = "Нажми меня... ";

        showNotification(title, text, link);

    }

    c.close();

    db.close();

    TIME += 500;

    AlarmReceiver.scheduleAlarms(this, TIME);

}

When an alarm is triggered, we request all tasks with time from the database TIME. The request db.select_ALARM_BY_UTIME(TIME) looks like this:

Code:
SELECT msg FROM tbSheduler WHERE utime = TIME

Www​

A ready-made class WakefulIntentServicethat preserves the semantics of use IntentService, but with a wake-up block on GitHub

The showNotification function displays a new notification in the status bar, which consists of two icons (application and notification icons), a title ( title), text ( text) and additional information (info):

Code:
private void showNotification(String title, String text, String info) {

   Intent intent = new Intent(this, Main.class);

   PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

   NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);

   Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);

   NotificationCompat.Builder b = new NotificationCompat.Builder(this);

   b.setAutoCancel(true)

    .setDefaults(Notification.DEFAULT_SOUND)

    .setContentTitle(title)

    .setContentText(text)

    .setContentInfo(info)

    .setContentIntent(pendingIntent)

    .setSmallIcon(R.drawable.ic_notify)

    .setLargeIcon(bm)

    .setWhen(System.currentTimeMillis())

    .setTicker(title);

   Notification n = b.getNotification();

   notificationManager.notify(NOTIFY_GROUP, n);

}

We use a constant Context.NOTIFICATION_SERVICE to request access to the system notification service. Next, we fill in all the fields that define the labels and icons (the differences between notifications in different versions of Android are shown in the screenshot), as well as the response time - setWhen(System.currentTimeMillis())that is, immediately. The method setContentIntent sets the pending intent that you already know to be called when the notification is clicked. In our case, we just run the single activity of our scheduler ( Main.java). The specified setDefaults constant Notification.DEFAULT_SOUND defines the standard beep as the notification sound.

fig5.png

Notice: Android 2 vs Android 4

As you may have noticed, we have processed only one task at a specific time, processing the rest will become your homework.

Icons in notifications​

When displaying notifications in the status bar, it would be nice to show the corresponding icon along with the text. Eclipse has a handy tool to help you create such an icon for all screen sizes and Android versions at once. To call it in the context menu of your project, select New → Other → Android Icon Set → Notification Icons. In the wizard that appears, open a graphic file or specify the required text (see screenshot). All possible types of your icon will be created automatically.

fig7.png

Create an icon

Having dealt with one alarm, we increase the time by half a second and call the extended version scheduleAlarms that the two tasks can be so close in time that the second simply will not work, since the first has not finished yet. By passing the time of the first one as a parameter with a small shift (at least 1 ms), we guarantee that the second task, albeit with a forced delay, will work. By the way, in our scheduler, tasks are set with an accuracy of the minute, but nothing prevents us from increasing the accuracy to seconds or even milliseconds.

fig6.png

Debug printing is indispensable for development

CONCLUSIONS​

Today we got acquainted with the mechanism of operation of the broadcast receiver in OS Android, launched a background service, did a little work with the database, learned how to display system notifications, and created a couple of pending intents. And of course, we have prepared a good scheduling framework for the scheduler, building on which you can create your own unique version of Cron.
 
Top