0

This is in relation to the question How to run a background task in android when the app is killed which I posted a few days ago.

After reading some blogs (e.g this) and code (of telephony) I have done some coding. My requirement is to show the user some sort of notification when a call is received, even when my app is not running (much like the Truecaller app in android). I am posting the code below.

callback_dispatcher.dart

const String HANDLE_INCOMING_CALL_BG = "handleIncomingCallBg";
const String INCOMING_CALL_BG_INITIALIZED = "incomingCallBgInitialized";
const String INCOMING_CALL_BG_INIT_SERVICE = "incomingCallBgInitService";
const String backgroundChannelId = "background_channel";
const String foregroundChannelId = "foreground_channel";

void callback_dispatcher() {
    const MethodChannel _backgroundChannel = MethodChannel(backgroundChannelId);
    WidgetsFlutterBinding.ensureInitialized();
    _backgroundChannel.setMethodCallHandler((MethodCall call) =>
    methodCallHandler(call));
    _backgroundChannel.invokeMethod<void>(INCOMING_CALL_BG_INITIALIZED);
}

Future<dynamic> methodCallHandler(MethodCall call) async {
    if(call.method == HANDLE_INCOMING_CALL_BG) {
        final CallbackHandle _handle =
        CallbackHandle.fromRawHandle(call.arguments['handle']);
        final Function _handlerFunc = PluginUtilities.getCallbackFromHandle(_handle)!;
        try {
            await _handlerFunc(call.arguments['message']);
        } catch(e) {
            print('Unable to handle incoming call in background');
            print(e);
        }
    }
    return Future<void>.value();
}

void handleBgMsg(String msg) {
    debugPrint("[handleBgMsg] incoming call number : " + msg);
    // notification to user
}

background_incoming_call.dart

typedef BgMessageHandler(String msg);

class BackgroundIncomingCall {
    MethodChannel? foregroundChannel;

    Future<void> initialize(BgMessageHandler onBgMessage) async {
        foregroundChannel = const MethodChannel(foregroundChannelId);
        final CallbackHandle? bgSetupHandle = 
        PluginUtilities.getCallbackHandle(callback_dispatcher);
        final CallbackHandle? bgMsgHandle = 
        PluginUtilities.getCallbackHandle(onBgMessage);
        await foregroundChannel?.invokeMethod<bool>(
            INCOMING_CALL_BG_INIT_SERVICE,
            <String, dynamic> {
            'bgSetupHandle': bgSetupHandle?.toRawHandle(),
            'bgMsgHandle': bgMsgHandle?.toRawHandle()
            }
        );
    }
}

In main.dart I am initializing BackgroundIncomingCall().initialize(handleBgMsg);.

On the Android part I have created a BroadcastReceiver as follows (the code mostly from the aforementioned Telephony package).

public class CallEventReceiver extends BroadcastReceiver implements
    MethodChannel.MethodCallHandler {
private static final String TAG = "[TEST]";
private static final int NUMBER_LEN = 10;

public static String INCOMING_CALL_BG_INITIALIZED = "incomingCallBgInitialized";
public static String INCOMING_CALL_BG_INIT_SERVICE = "incomingCallBgInitService";
public static String SETUP_HANDLE = "bgSetupHandle";
public static String MESSAGE_HANDLE = "bgMsgHandle";
public static String BG_INCOMING_CALL_CHANNEL_ID = "background_channel";
public static String FG_INCOMING_CALL_CHANNEL_ID = "foreground_channel";
public static String HANDLE_INCOMING_CALL_BG = "handleIncomingCallBg";
public static String SHARED_PREF_NAME = "incomingCallSharedPrefBg";
public static String SHARED_PREF_BG_INCOMING_CALL_HANDLE = "incomingCallSharedPrefBgHandle";
public static String SHARED_PREFS_BACKGROUND_SETUP_HANDLE = "backgroundSetupHandle";

private Activity activity = null;

/* for background isolate*/
private Context appContext = null;
private AtomicBoolean isIsolateRunning = new AtomicBoolean(false);
private List<String> bgIncomingCallQueue = Collections.synchronizedList(new ArrayList<String>());
private MethodChannel bgIncomingCallChannel = null;;
private MethodChannel fgIncomingCallChannel = null;;
private FlutterLoader flutterLoader = null;
private FlutterEngine bgFlutterEngine = null;
private Long bgIncomingCallHandle = null;
/* END */

public CallEventReceiver() {}

public CallEventReceiver(Activity activity) {
    super();
    this.activity = activity;
}

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
    if(call.method.equals(INCOMING_CALL_BG_INITIALIZED)) {
        if(appContext != null) {
            onChannelInitialized(appContext);
        } else {
            throw new RuntimeException("Application context is not set");
        }
    } else if(call.method.equals(INCOMING_CALL_BG_INIT_SERVICE)) {
        if(appContext != null) {
            if(call.hasArgument(SETUP_HANDLE) && call.hasArgument(MESSAGE_HANDLE)) {
                Long setupHandle = call.<Long>argument(SETUP_HANDLE);
                Long msgHandle = call.<Long>argument(MESSAGE_HANDLE);
                if (setupHandle == null || msgHandle == null) {
                    result.error("ILLEGAL_ARGS", "Setup handle or message handle is missing", null);
                    return;
                }
                setBgSetupHandle(appContext, setupHandle);
                setBgMessageHandle(appContext, msgHandle);
            }
        } else {
            throw new RuntimeException("Application context is not set");
        }
    } else {
        result.notImplemented();
    }
}

@Override
public void onReceive(Context context, Intent intent) {
    this.appContext = context;
    try {
        Log.i(TAG, "[CallEventHandler] Receiver start");
        String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
        String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
        if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
            if(incomingNumber != null) {
                Log.i(TAG, "[CallEventHandler] Incoming number : " + incomingNumber);
                if(incomingNumber.length() > NUMBER_LEN) {
                    incomingNumber = incomingNumber.substring(incomingNumber.length() - NUMBER_LEN, incomingNumber.length());
                    Log.i(TAG, "[CallEventHandler] Incoming number after : " + incomingNumber);
                    processIncomingCall(context, incomingNumber);
                }
            }
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

public void setBgMessageHandle(Context ctx, Long handle) {
    bgIncomingCallHandle = handle;
    SharedPreferences pref = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
    pref.edit().putLong(SHARED_PREF_BG_INCOMING_CALL_HANDLE, handle).apply();
}

public void setBgSetupHandle(Context ctx, Long setupBgHandle) {
    SharedPreferences pref = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
    pref.edit().putLong(SHARED_PREFS_BACKGROUND_SETUP_HANDLE, setupBgHandle).apply();
}

private void processIncomingCall(Context ctx, String incomingNumber) {
    boolean isForeground = isAppForeground(ctx);
    Log.i(TAG, "[processIncomingCall] is app in foreground : " + isForeground);
    if (isForeground) {
        processIncomingCallInForeground(incomingNumber);
    } else {
        processIncomingCallInBackground(ctx, incomingNumber);
    }
}

private void processIncomingCallInBackground(Context ctx, String incomingNumber) {
    if(!isIsolateRunning.get()) {
        init(ctx);
        SharedPreferences sharedPref = ctx.getSharedPreferences(SHARED_PREF_NAME,
                Context.MODE_PRIVATE);
        Long bgCallbackHandle = sharedPref.getLong(SHARED_PREFS_BACKGROUND_SETUP_HANDLE, 0);
        startBackgroundIsolate(ctx, bgCallbackHandle);
        bgIncomingCallQueue.add(incomingNumber);
    } else {
        executeDartCallbackInBgIsolate(ctx, incomingNumber);
    }
}

private void processIncomingCallInForeground(String incomingNumber) {
    Log.i(TAG, "[processIncomingCallInFg] incoming number : " + incomingNumber);
    if(activity != null) {
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(eventSink != null) {
                    eventSink.success(incomingNumber);
                }
            }
        });
    }
}

private void onChannelInitialized(Context ctx) {
    isIsolateRunning.set(true);
    synchronized(bgIncomingCallQueue) {
        Iterator<String> it = bgIncomingCallQueue.iterator();
        while(it.hasNext()) {
            executeDartCallbackInBgIsolate(ctx, it.next());
        }
        bgIncomingCallQueue.clear();
    }
}

private void init(Context ctx) {
    FlutterInjector flutterInjector = FlutterInjector.instance();
    flutterLoader = flutterInjector.flutterLoader();
    flutterLoader.startInitialization(ctx);
}

private void executeDartCallbackInBgIsolate(Context ctx, String incomingNumber) {
    if(bgIncomingCallChannel == null) {
        throw new RuntimeException("background channel is not initialized");
    }

    if(bgIncomingCallHandle == null) {
        bgIncomingCallHandle = getBgMessageHandle(ctx);
    }
    HashMap<String, Object> args = new HashMap<String, Object>();
    args.put("handle", bgIncomingCallHandle);
    args.put("message", incomingNumber);
    bgIncomingCallChannel.invokeMethod(HANDLE_INCOMING_CALL_BG, args);
}

private Long getBgMessageHandle(Context ctx) {
    return ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
            .getLong(SHARED_PREF_BG_INCOMING_CALL_HANDLE, 0);
}

private void startBackgroundIsolate(Context ctx, Long callbackHandle) {
    String appBundlePath = flutterLoader.findAppBundlePath();
    FlutterCallbackInformation cbInfo = FlutterCallbackInformation.
            lookupCallbackInformation(callbackHandle);

    DartExecutor.DartCallback dartEntryPoint =
            new DartExecutor.DartCallback(ctx.getAssets(), appBundlePath, cbInfo);
    bgFlutterEngine = new FlutterEngine(ctx, flutterLoader, new FlutterJNI());
    bgFlutterEngine.getDartExecutor().executeDartCallback(dartEntryPoint);

    bgIncomingCallChannel = new MethodChannel(bgFlutterEngine.getDartExecutor(),
            BG_INCOMING_CALL_CHANNEL_ID);
    bgIncomingCallChannel.setMethodCallHandler(this);
}

private boolean isAppForeground(Context ctx) {
    KeyguardManager keyGuardManager = (KeyguardManager) ctx.
            getSystemService(Context.KEYGUARD_SERVICE);
    if(keyGuardManager.isKeyguardLocked()) {
        return false;
    }
    int pid = Process.myPid();
    ActivityManager activityManager = (ActivityManager) ctx.
            getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningAppProcessInfo> list =
            activityManager.getRunningAppProcesses();
    for(ActivityManager.RunningAppProcessInfo proc : list) {
        if(pid == proc.pid) {
            return proc.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
        }
    }
    return false;
}

}

In my AndroidManifest.xml I am registering the broadcast receiver

<receiver android:name="org.cdot.diu.handler.CallEventReceiver" android:exported="true">
       <intent-filter>
           <action android:name="android.intent.action.PHONE_STATE"/>
       </intent-filter>
</receiver>

Now If I run my app and minimize it, the handleBgMsg is called successfully. Since no print or debug message is shown in terminal when I kill the app, I am sending an SMS to me using Telephony package, which is sent successfully when the app is minimized. But it does not work if I kill the application, no SMS is sent. I am not sure what I am doing wrong here. My guess is that the Broadcast Receiver is killed when the app is killed. However, in the telephony package there is nothing to indicate that special care is done to keep the receiver alive when the app is killed.

Sudipta Roy
  • 740
  • 1
  • 9
  • 29
  • If you are building a calling app, did you see: https://developer.android.com/guide/topics/connectivity/telecom/selfManaged – Morrison Chang May 27 '22 at 09:01
  • No I am not trying to build a calling app. My main motivation is to see how these background task are done when the app is not running, in flutter. For testing I am receiveing the call events in my `BroadcastReceiver` which is working as it is. The telephony package also gets incoming messages when the app is not running. – Sudipta Roy May 27 '22 at 09:09

0 Answers0