Android Custom Notification Layout - transistorsoft/capacitor-background-geolocation GitHub Wiki

On Android, the BackgroundGeolocation SDK tracks from a foreground service, and Android requires a foreground service to show a persistent notification. If the default notification doesn't suit your needs (eg: you want to add custom buttons), you can design your own notification layout:

All the notification options on this page live under app.notification in your config.

Step 1 — Create a custom layout file:

Open your app's Android project in Android Studio (npx cap open android). Select File->New->XML->Layout XML File:

Enter any layout name (eg: notification_layout). Your file will be created in the folder android/app/src/main/res/layout:

Step 2 — Edit your layout:

Even if you have no experience with Android Layouts, it doesn't take much to figure out the basics. You'll mostly be adding <TextView />, <ImageView /> and <Button /> elements inside a <LinearLayout />. Android draws a notification layout with RemoteViews, which supports only a limited set of views (for example LinearLayout, FrameLayout, RelativeLayout, TextView, ImageView and Button). Other views, such as ConstraintLayout or your own custom views, aren't supported.

The key thing to be aware of is each element's android:id, which is how the plugin finds it:

  • A few special ids are filled in by the plugin automatically (see the table below).
  • Your own elements can use any android:id you like. You refer to them by that id in app.notification.strings (text) and app.notification.actions (buttons).

Layout Special Elements

When BackgroundGeolocation renders your custom notification layout, it queries for the following elements by their android:id. When it finds one, it fills that element from the corresponding data source:

Layout element android:id Data source
applicationName Your app's name (android:label in AndroidManifest.xml)
notificationTitle app.notification.title
notificationText app.notification.text
notificationSmallIcon app.notification.smallIcon (defaults to your app icon)
notificationLargeIcon app.notification.largeIcon (filled only when you set largeIcon)
BackgroundGeolocation.ready({
  app: {
    notification: {
      layout: 'notification_layout',     // <-- your layout file, without .xml
      title: 'The Notification Title',   // --> @+id/notificationTitle
      text: 'The Notification Text',     // --> @+id/notificationText
      smallIcon: 'mipmap/my_small_icon', // --> @+id/notificationSmallIcon (defaults to your app icon)
      largeIcon: 'mipmap/my_large_icon'  // --> @+id/notificationLargeIcon
    }
  }
});

Custom <TextView /> Elements

You can declare your own custom <TextView /> elements and render text into them using the app.notification.strings parameter.

<!-- Its id, myCustomElement, is the key you'll use in app.notification.strings -->
<TextView
    android:id="@+id/myCustomElement"
    style="@style/TextAppearance.Compat.Notification.Line2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="myCustomElement"
    android:textSize="12sp" />

Provide the text for your custom elements with app.notification.strings. Each key is the android:id of a <TextView />:

BackgroundGeolocation.ready({
  app: {
    notification: {
      layout: 'notification_layout',
      strings: {
        myCustomElement: 'My Custom Element Text'  // --> @+id/myCustomElement
      }
    }
  }
});

Custom <Button /> Elements:

You can declare your own custom <Button /> elements and register click-listeners upon them using the app.notification.actions parameter:

<!-- Its id, notificationButtonFoo, is the name you'll list in app.notification.actions -->
<Button
    android:id="@+id/notificationButtonFoo"
    style="@style/Widget.AppCompat.Button.Small"
    android:layout_width="60dp"
    android:layout_height="40dp"
    android:text="Foo" />

Register listeners for your buttons using app.notification.actions, and receive the clicks with onNotificationAction. The callback receives the android:id of the button that was clicked:

BackgroundGeolocation.ready({
  app: {
    notification: {
      layout: 'notification_layout',
      actions: [  // <-- the android:id of each button to listen to
        'notificationButtonFoo',
        'notificationButtonBar'
      ]
    }
  }
});

// Listen to custom button clicks:
BackgroundGeolocation.onNotificationAction((buttonId) => {
  console.log('[onNotificationAction]', buttonId);
  switch (buttonId) {
    case 'notificationButtonFoo':
      break;
    case 'notificationButtonBar':
      break;
  }
});

While your app is terminated, there is no JavaScript to receive the click. With app.stopOnTerminate: false and app.enableHeadless: true, the click is delivered instead to your Android headless task as a notificationaction event. (With the default stopOnTerminate: true, terminating your app stops tracking and removes the notification.) See Android Headless Mode.

Sample Layout

As a starting-point for your custom layout, copy the following content into your new file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="135dp"
    android:gravity="start"
    android:orientation="vertical"
    android:padding="15dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="15dp"
        android:gravity="center"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/notificationSmallIcon"
            android:layout_width="16dp"
            android:layout_height="16dp"
            android:tint="@android:color/background_dark"
            tools:srcCompat="@tools:sample/avatars" />

        <TextView
            android:id="@+id/applicationName"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingLeft="10dp"
            android:text="applicationName"
            android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
            android:textColor="#888888"
            android:textSize="12sp" />
    </LinearLayout>

    <TextView
        android:id="@+id/notificationTitle"
        style="@style/TextAppearance.Compat.Notification.Title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="notificationTitle"
        android:textSize="14sp" />

    <TextView
        android:id="@+id/notificationText"
        style="@style/TextAppearance.Compat.Notification.Line2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="notificationText"
        android:textSize="14sp" />

    <TextView
        android:id="@+id/myCustomElement"
        style="@style/TextAppearance.Compat.Notification.Line2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="myCustomElement"
        android:textSize="12sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:orientation="horizontal">

        <Button
            android:id="@+id/notificationButtonFoo"
            style="@style/Widget.AppCompat.Button.Small"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:text="Foo" />

        <Button
            android:id="@+id/notificationButtonBar"
            style="@style/Widget.AppCompat.Button.Small"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:text="Bar" />
    </LinearLayout>
</LinearLayout>

The screenshot below maps the elements of a similar layout to the rendered notification:

Step 3 — Using your custom layout:

BackgroundGeolocation.ready({
  app: {
    notification: {
      layout: 'notification_layout',  // <-- the name of your file (without .xml)
      title: 'The title',
      text: 'The text',
      strings: {
        myCustomElement: 'custom TextView element'
      },
      actions: [
        'notificationButtonFoo',  // <-- register button click-listeners
        'notificationButtonBar'
      ]
    }
  }
});

// Listen to custom notification button clicks (app.notification.actions)
BackgroundGeolocation.onNotificationAction((buttonId) => {
  console.log('[onNotificationAction]', buttonId);
  switch (buttonId) {
    case 'notificationButtonFoo':
      // Handle button click on [Foo]
      break;
    case 'notificationButtonBar':
      // Handle button click on [Bar]
      break;
  }
});

Troubleshooting

If something doesn't appear, set logger: { logLevel: BackgroundGeolocation.LogLevel.Warning } (or higher, such as Verbose) and watch adb logcat -s TSLocationManager. These messages are warnings and errors; at the default logLevel, Off, none of them reach logcat. The plugin reports a layout problem with one of these messages:

Message Cause
Could not find custom notification layout '<layout>' in app/src/main/res/layout No layout file with that name. Use the file name without .xml. The plugin falls back to its default notification.
Failed to find TextView resource: <key> A key in app.notification.strings doesn't match any android:id in your app.
Failed to find Button resource in notification_layout for notification-action: <id> An entry in app.notification.actions doesn't match any android:id in your app.
Failed to find ImageView id: notificationLargeIcon in notification layout You set largeIcon, but no element in your app has the android:id notificationLargeIcon.
Failed to decode app.notification.largeIcon (vector/xml requires Drawable): <name> No drawable or mipmap resource matches app.notification.largeIcon, or it couldn't be loaded.
Failed to resolve app.notification.smallIcon: <name> No drawable or mipmap resource matches app.notification.smallIcon. The SDK uses your app icon instead.
⚠️ **GitHub.com Fallback** ⚠️