App Inventor Extensions


Notification Listener Extension SDK 34 ready!

See the App Inventor Extensions document about how to use an App Inventor Extension.

For questions about this extension or bug reports please start a new thread in the App Inventor community. Thank you.

For feature requests please contact me by email. To be a sponsor of a new method already is possible starting from only 10 USD! With your contribution you will help the complete App Inventor community. Thank you.

Nov 8th, 2020: Version 1: initial version for App Inventor.

Aug 16th, 2022: Version 2: filter by package name added, SDK31 update: exported = "true" added in manifest for serviceelement

Aug 10th, 2023: Version 3: Google Pay fix by vknow360

Jan 20th, 2024: Version 4: replace local broadcast receiver by normal broadcast receiver by Kumaraswamy

Jan 21th, 2024: Version 5: framework wrapper added by Kumaraswamy, minor improvements

Jul 29th, 2024: Version 6: SDK 34 update, minor improvements

Description

Notification Listener Extension to listen to all notifications of your device. Packagename, title and text of all notifications will be stored in TinyDB. Also of course works if your app is not running and also survives a reboot of the device. Starting form Version 5 the itoox-wrapper library from Kumaraswamy is used, which helps to directly invoke Itoo (if present) to call procedures from the background. No persistent background service or persistent notification is required anymore!
Minimum API level is 21 (Android 5).
Required permissions: android.permission.BIND_NOTIFICATION_LISTENER_SERVICE

Note: It has been reported, that the extension does not work on MIUI.

Properties



Package name filter in csv format separated by comma.



Reference screen for Itoo where the procedures 'OnCreate' and 'OnNotificationReceived' exist. It is advised to set the Reference screen property before Starting the service initially. Or if no property is set, the extension will take "Screen1" as the reference by default.

OnCreate does not have an argument, OnNotificationReceived has the arguments packageName, title and text.
How this can be used please see the Notificationlistener extenion and Itoo tutorial.


Methods


Start Service.
This method will open the settings of the device. The user must choose the app from the app list and enable notification access. A warning message will be displayed and the user has to confirm, see screehshot.

Note: You will have to build the app to be able to receive the notifications.
Note: To stop the service, just disable notification access again.

How does it work?
The notification listener service runs in the backround also if your app is closed. It will listen to all notifications and stores them in TinyDB (aka shared preferences) using the name space TaifunNotificationListener. As tag the current datetime will be used in format yyyy-MM-dd HH:mm:ss, as value a JSON string will be stored including the information packagename, title and text. You can use TinyDB blocks to get the information (see screenshot of the blocks below).


Returns true if notification listener is enabled, else false.

Events


Event indicating that a notification has been received.
If your app is up and running, new notifications can be received via this event.

Note: You will have to build the app to be able to receive the notifications.

Example App "Notification Listener Test"

Screenshots:

While starting the service, the user needs to confirm.

If the app is open, you can receive the latest notification in event Received.

If the app is closed, later you can open the app and get a list of all notifications using TinyDB blocks.

Blocks:


Test

Tested successfully on Samsung Galaxy A51 running on Android 10. Version 5 tested successfully on Samsung Galaxy A54 running on Androdi 14.

Questions and Answers

Q1: Is it possible for make the extension working in debug mode? I have problems in developing my app, because I have to compile the code each time to get a good code.
A: Unfortunately it is not possible to test extensions, which use a service like the notification listener extension in the companion app. This is a technical restriction of the companion app.
But for easier developing of your app, just modify the example project and add a button to export the TinyDB into a file. You could use the Get all TinyDB Extension by Juan Antonio to backup TinyDB as Text or JSON format. Let the example project run for a while until you collected several notifications and then export TinyDB. Then in your app you like to develop import the stored TinyDB and work with the previously stored notifications.

Q2: I want to save the json response into 3 lists. Can you please help me how can i achieve it?
A: See the answer from ABG here here. Thank you ABG!

For questions about App Inventor,
please ask in the App Inventor community.Thank you.

Java source code

The source code is open source. Please read the Creative Commons Attribution-ShareAlike 3.0 Unported License to find out, what is allowed and under which terms. Thank you.

// -*- mode: java; c-basic-offset: 2; -*-
package com.puravidaapps.TaifunNotificationListener;
// Version 1: initial version
// Version 2: filter by package name, SDK31 update: exported = "true"
// Version 3: Google Pay fix by vknow360
// Version 4: replace local broadcast receiver by normal broadcast receiver by Kumaraswamy, OnResume and OnStop events removed
// Version 5: framework wrapper added by Kumaraswamy, minor improvements
// Version 6: SDK 34 update, registerReceiver(NotificationReceiver, new IntentFilter("LOCAL"), RECEIVER_NOT_EXPORTED);



/**
  * Copyright (C) 2020 Pura Vida Apps - All Rights Reserved
  *
  * This work by Pura Vida Apps is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License
  * with attribution (name=Pura Vida Apps and link to the source site=https://puravidaapps.com/notificationlistener.php required.
  * You may use, distribute and modify this code under the terms of the license, see link above. Please keep this header inside the source code.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  *
  * Author: Taifun, puravidaapps.com
  * Date:   2020-11-08
  *
  * Note:   This example is based on this Stackoverflow answer by Mayur Patel. Thank you Mayur!
  *
  */



import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;

import java.util.Calendar;
import org.json.JSONObject;
import xyz.kumaraswamy.itoox.wrapper.FrameworkWrapper;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesServices;
import com.google.appinventor.components.annotations.androidmanifest.*;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.Component;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.EventDispatcher;
import com.google.appinventor.components.runtime.OnResumeListener;
import com.google.appinventor.components.runtime.OnStopListener;
import com.google.appinventor.components.runtime.util.Dates;


@DesignerComponent(version = TaifunNotificationListener.VERSION,
    description = "TaifunNotificationListener Extension. Version 5 as of 2024-01-22.",
    category = ComponentCategory.EXTENSION,
    nonVisible = true,
    androidMinSdk = 21,
    iconName = "https://puravidaapps.com/images/taifun16.png",
    helpUrl = "https://puravidaapps.com/notificationlistener.php")
@SimpleObject(external = true)
@UsesServices(services = {
    @ServiceElement(
        name = "com.puravidaapps.TaifunNotificationListener.TaifunNotificationListener$NotificationService",
        exported = "true",
        label = "NotificationService",
        permission = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE",
        intentFilters = {
            @IntentFilterElement(actionElements = {@ActionElement(name = "android.service.notification.NotificationListenerService")
        })
    })
})
@UsesLibraries(libraries = "itoox-wrapper.jar") // 2024-01-21

public class TaifunNotificationListener extends AndroidNonvisibleComponent implements Component, OnStopListener, OnResumeListener {

  public static final int VERSION = 5;
  private Context context;
  private Activity activity;
  private static final String LOG_TAG = "TaifunNotificationList";
  private ComponentContainer container;
  public static SharedPreferences sharedPreferences;
  public static SharedPreferences sharedPreferencesSetup; // 2021-08-06
  public static SharedPreferences.Editor editor;          // 2021-08-06
  private boolean listening = false;

  /**
   * Creates a new TaifunNotificationListener component.
   *
   * @param container
   */
  public TaifunNotificationListener(ComponentContainer container) {
    super(container.$form());
    this.container = container;
    context = (Context) container.$context();
    activity = (Activity) container.$context();
    sharedPreferences = context.getSharedPreferences("TaifunNotificationListener", Context.MODE_PRIVATE);
    sharedPreferencesSetup = context.getSharedPreferences("TaifunNotificationListenerSetup", Context.MODE_PRIVATE); // 2021-08-06
    editor = sharedPreferencesSetup.edit();

    form.registerForOnResume(this); // 2024-01-21
    form.registerForOnStop(this);   // 2024-01-21

    startListeningLocal();
    Log.d(LOG_TAG, "TaifunNotificationListener Created");
  }

  /**
   * PackageNamesFilter
   */
  @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "Package name filter in csv format separated by comma.")
  public String PackageNamesFilter() {
    return sharedPreferencesSetup.getString("packageNamesFilter", "");
  }

  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
  @SimpleProperty
  public void PackageNamesFilter(final String packageNamesFilter) {
    editor.putString("packageNamesFilter", packageNamesFilter).commit();
  }

  /**
   * ReferenceScreen
   */
  @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "Reference screen for Itoo where the procedures 'OnCreate' and 'OnNotificationReceived' exist. " +
      "'OnCreate' does not have an argument, 'OnNotificationReceived' has the arguments packageName, title and text.")
  public String ReferenceScreen() {
    return sharedPreferencesSetup.getString("referenceScreen", "Screen1");
  }

  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "Screen1")
  @SimpleProperty
  public void ReferenceScreen(String referenceScreen) {
    editor.putString("referenceScreen", referenceScreen).commit();
  }


  /**
   * StartService
   */
  @SimpleFunction(description = "Start Service.")
  public void StartService() {
    Log.d(LOG_TAG, "StartService");

    Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
    context.startActivity(intent);
  }


  /**
   * CheckService
   */
  @SuppressLint("NewApi")
  @SimpleFunction(description = "Returns true if notification listener is enabled, else false.")
  public boolean CheckService() {
    try {
      String packageName = context.getPackageName();
      if(Settings.Secure.getString(activity.getContentResolver(), "enabled_notification_listeners").contains(packageName)) {
        return true;
      } else {
        return false;
      }
    } catch(Exception e) {
      Log.e(LOG_TAG, e.getMessage(), e);
      e.printStackTrace();
    }
    return false;
  }


  private void startListeningLocal() {
    if (!listening) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        // 2024-06-29 SDK 34 update: https://stackoverflow.com/a/77276774 RECEIVER_EXPORTED
        form.registerReceiver(NotificationReceiver, new IntentFilter("LOCAL"), RECEIVER_NOT_EXPORTED);
      } else {
        form.registerReceiver(NotificationReceiver, new IntentFilter("LOCAL"));  // 2024-01-20 by Kumaraswamy
      }
      listening = true;
     }
  }


  private void stopListeningLocal() {
    if (listening) {
      // Unregister broadcast listener
      form.unregisterReceiver(NotificationReceiver);  // 2024-01-20 by Kumaraswamy
      listening = false;
    }
  }

  private final BroadcastReceiver NotificationReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      Log.d(LOG_TAG, "NotificationReceiver.onReceive");
      try {
        String json = intent.getStringExtra("json");
        NotificationReceived(intent.getStringExtra("packageName"),
            intent.getStringExtra("title"), intent.getStringExtra("text")); // 2024-01-21
      } catch (Exception e) {
        Log.e(LOG_TAG, e.getMessage(), e);
        e.printStackTrace();
      }
    }
  };

  /**
   *  NotificationReceived event handler.
   */
  @SimpleEvent (description = "Event indicating that a notification has been received.")
  public void NotificationReceived(String packageName, String title, String text) {
    Log.d(LOG_TAG, "NotificationReceived, packageName: " + packageName + ", title: " + title + ", text: " + text);
    EventDispatcher.dispatchEvent(this, "NotificationReceived", packageName, title, text);
  }

  @Override
  public void onResume() {
    Log.i(LOG_TAG, "onResume");
    startListeningLocal();
  }

  @Override
  public void onStop() {
    Log.i(LOG_TAG, "onStop");
    stopListeningLocal();
  }


  // https://stackoverflow.com/a/56424417/1545993
  @SuppressLint("NewApi")
  public static class NotificationService extends NotificationListenerService {
    private static Context context;
    private String packageName = "";
    private String title = "";
    private String text = "";
    private String packageNamesFilter = "";
    private FrameworkWrapper wrapper; // 2024-01-21 by Kumaraswamy

    @Override
    public void onCreate() {
      Log.i(LOG_TAG,"onCreate");
      super.onCreate();
      context = getApplicationContext();

      // 2024-01-21 by Kumaraswamy
      if (FrameworkWrapper.isItooXPresent()) {
        this.wrapper = new FrameworkWrapper(context, getReferenceScreen());
        if (this.wrapper.success())
          this.wrapper.call("OnCreate");
      }
      // 2024-01-21 by Kumaraswamy
    }

    @Override
    public void onListenerConnected() {
      Log.i(LOG_TAG,"onListenerConnected");
    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
      packageName = sbn.getPackageName();
      Log.i(LOG_TAG,"onNotificationPosted: " + packageName);

      sharedPreferencesSetup = context.getSharedPreferences("TaifunNotificationListenerSetup", Context.MODE_PRIVATE);
      packageNamesFilter = sharedPreferencesSetup.getString("packageNamesFilter", "");
      Log.v(LOG_TAG,"packageNamesFilter: " + packageNamesFilter);

      if (packageNamesFilter.contains(packageName) || packageNamesFilter.equals("") ) {
        Calendar now = Calendar.getInstance();
        JSONObject json = new JSONObject();
        getDetails(sbn);
        try {
          json.put("packageName", packageName);
          json.put("title", title);
          json.put("text", text);
        } catch (Exception e) {
          Log.e(LOG_TAG, e.getMessage(), e);
          e.printStackTrace();
        }
        String jsonString = json.toString();      // 2024-01-21

        if (title.equals("null")) { // listen to a WhatsApp voice message
          Log.i(LOG_TAG,"ignored: packageName: " + packageName + ", title: " + title + ", text: " + text);
        } else {
          // save data in TinyDB aka shared preferences, tag is now, value is the json string
          sharedPreferences = context.getSharedPreferences("TaifunNotificationListener", Context.MODE_PRIVATE);
          SharedPreferences.Editor editor = sharedPreferences.edit();
          editor.putString(Dates.FormatDateTime(now, "yyyy-MM-dd HH:mm:ss"), jsonString).commit();

          // 2024-01-21 by Kumaraswamy
          if (this.wrapper != null && this.wrapper.success())
            this.wrapper.call("OnNotificationReceived", packageName, title, text);
          // 2024-01-21 by Kumaraswamy

          // send local broadcast to main activity
          Intent intent = new Intent("LOCAL");
          intent.putExtra("packageName", packageName); // 2024-01-21
          intent.putExtra("title", title);             // 2024-01-21
          intent.putExtra("text", text);               // 2024-01-21
          context.sendBroadcast(intent); // 2024-01-20 by Kumaraswamy
        }
      } else {
        Log.i(LOG_TAG, "ignored: " + packageName);
      }
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
      packageName = sbn.getPackageName();
      Log.i(LOG_TAG,"onNotificationRemoved: " + packageName);

      // 2024-01-21 by Kumaraswamy
      if (this.wrapper != null && this.wrapper.success())
        this.wrapper.call("OnNotificationRemoved", packageName);
      // 2024-01-21 by Kumaraswamy
    }

    // 2024-01-21 by Kumaraswamy
    private String getReferenceScreen() {
      SharedPreferences sharedPreferences = context.getSharedPreferences("TaifunNotificationListenerSetup", Context.MODE_PRIVATE);
      String referenceScreen = sharedPreferences.getString("referenceScreen", "Screen1");
      Log.d(LOG_TAG, "referenceScreen: " + referenceScreen);
      return referenceScreen;
    }

    private void getDetails(StatusBarNotification sbn) {
      Log.v(LOG_TAG,"getDetails");
      Bundle extras = sbn.getNotification().extras;

      title = "";
      if (extras.getString("android.title") != null) {
        title = extras.getString("android.title");
      } else {
        // 2023-08-10 Google Pay fix by vknow360 found here https://stackoverflow.com/a/61736259/1545993
        title = String.valueOf(extras.get("android.title"));
      }

      text = "";
      if (extras.getCharSequence("android.text") != null) {
        text = extras.getCharSequence("android.text").toString();
      } else {
        // 2023-08-10
        title = String.valueOf(extras.getCharSequence("android.text"));
      }

      Log.v(LOG_TAG,"packageName: " + packageName + ", title: " + title + ", text: " + text);
    }
  }

}

Terms and Conditions

Download


Developing and maintaining snippets, tutorials and extensions for App Inventor takes a lot of time.
I hope it saved some of your time. If yes, then you might consider to donate a small amount!

Donation amount:

or donate some mBTC to Bitcoin Address:
1Jd8kXLHu2Vkuhi15TWHiQm4uE9AGPYxi8
Bitcoin

Thank you! Taifun
 

Download TaifunNotificationListener extension (aix file)
Download Notification Listener Test project (aia file)
Back to top of page ...

Creative Commons License
This work by Pura Vida Apps is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License
with attribution (name=Pura Vida Apps and link to the source site) required.

Back to top of page ...


Home | Snippets | Tutorials | Extensions | Links | Search | Privacy Policy | Contact