0

Setup:

Components:

  • App A that has a:

    1. web server that runs in its own thread (started in a service). Server sends intents to app B

    2. broadcast listener that listens for broadcast events from app B

  • App B that has a:

    1. broadcast listener that listens for intents from app A and sends intents back to it (with an extra (JSON) payload)

Execution flow:

  • app A is started, it starts a service in a separate thread in MainActivity
  • the service then starts a webserver that runs in its own threads and (the service) continues running in the background
  • when receiving a request the webserver sends an intent to app B and waits for a period of time
  • app B sends a response
  • the broadcast receiver in app A writes the intent extras to a file
  • the webserver now checks whether the file exists and sends a response back to the client accordingly

The webserver is started in a "regular" service (WebService:

    public class WebService extends Service {

    private static final String TAG = WebService.class.getSimpleName();

    private Looper serviceLooper;
    private ServiceHandler serviceHandler;

    private static final String extensionId = "";
    private static final String receiverServiceAppId = "com.cam.receiverservice";

    private static final String intentChannelsJsonKey = "Channels";

    // Handler that receives messages from the thread
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
            Log.d(TAG, "Created service handler");
        }

        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "Received message");
        }
    }

    @Override
    public void onCreate() {
        Log.d(TAG, "Creating service");
        // Start up the thread running the service. Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block. We also make it
        // background priority so CPU-intensive work doesn't disrupt our UI.
        HandlerThread thread = new HandlerThread("ServiceStartArguments",
                Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler
        serviceLooper = thread.getLooper();
        serviceHandler = new ServiceHandler(serviceLooper);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "Starting web server");
        WebServer webServer;
        int portNumber = 8080;
        try {
            // WEBSERVER STARTED HERE
            webServer = new WebServer(getApplicationContext(), portNumber);
        } catch (IOException e) {
            Log.d(TAG, "\nCouldn't start web server on port " + portNumber + "\n");
            e.printStackTrace();
        }
        Log.d(TAG, "Started web server");

        // For each start request, send a message to start a job and deliver the
        // start ID so we know which request we're stopping when we finish the job
        Message msg = serviceHandler.obtainMessage();
        msg.arg1 = startId;
        serviceHandler.sendMessage(msg);

        // If we get killed, after returning from here, restart
        return START_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "Binding service");
        // We don't provide binding, so return null
        return null;
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "Destroying service");
        Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
    }
    }

).

The broadcast receiver in app A writes the extras of the intent it receives from app B to a file:

public class AppABroadcastReceiver extends BroadcastReceiver {
    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onReceive(Context context, Intent intent) {
        // writes contents of intent to file
        handleReceiver(Constants.RECEIVER_TYPE.RESPONSE_RECEIVER, context, intent);
    }
}

The webserver started by the service:

public class WebServer extends NanoHTTPD {

    private static final String TAG = WebServer.class.getName();
    private static final int PORT_NO = 8080;
    private int portNumber = PORT_NO;
    private Context context;
    private String cwd;

    public WebServer(Context context) throws IOException {
        super(PORT_NO);
        this.context = context;
        start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
        cwd =  context.getApplicationInfo().dataDir;
        Log.d(TAG, "\nWeb server started at http://localhost:" + PORT_NO + "/ \n");
    }

    public WebServer(Context context, int portNumber) throws IOException {
        super(portNumber);
        this.context = context;
        start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
        cwd =  context.getApplicationInfo().dataDir;
        Log.d(TAG, "\nWeb server started at http://localhost:" + portNumber + "/ \n");
    }

    public int getPortNumber() {
        return portNumber;
    }

    private void sendIntent(String data) {
        Intent intent = new Intent();

        Log.d(TAG, "Creating intent to request data from app B");
        intent.setAction(APP_B_ACTION_NAME);
        intent.putExtra(REQ_KEY_NAME, data);
        context.sendBroadcast(intent);
        Log.d(TAG, "GET Data req intent sent");     
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public Response serve(IHTTPSession session) {  
        Map<String, String> params = session.getParms();

        if (params.get(REQ_KEY_NAME) != null) {
            // send intent to req data to app B
            sendIntent(Constants.REQ_TYPE.GET_USER_DATA, params.get(REQ_KEY_NAME));
            // wait for response
            sleepThread(3);
            // check file system for response
            String responseData = checkResponse(cwd);
            // check for possible timeouts/issues
            if (responseData == null || responseData.isEmpty()) {
                return newFixedLengthResponse(requestTimedOutJSON);
            } else {
                return newFixedLengthResponse(responseData);
            }
        }

        return newFixedLengthResponse(invalidRequestTypeJSON);
    }
}

all logic in the webserver takes place in server(). Looking at the differences between a Service and an IntentService,

Service vs IntentService in the Android platform

the IntentService queues intents and processes them in sequence:

https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/app/IntentService.java

yup I commented too soon. I was doing the work onStartCommand instead of onHandleIntent-- looks like onStartCommand is run on the UI thread, however a separate thread is spawned for onHandleIntent execution

IntentService calls that itself, after onHandleIntent() returns, if there is no more work to be done.

The question is:

If the Service were to be replaced by an IntentService, could the webserver be started in startService() and then, later, it would check for intents that would arrive at onHandleIntent() from the broadcast receiver (this is all in app A)?

The intent service has a server object, it initializes and starts it in startService(). Then it periodically calls, say, a setter method on the webserver object as it receives intents in onHandleIntent().

In other words, does launching the server count as an individual unit of work that must be finished by the intent service in order to proceed with another (e.g. intents received from the broadcast receiver)?

Sebi
  • 4,262
  • 13
  • 60
  • 116
  • `IntentService` is deprecated. Also, `IntentService` handles one `Intent`, then shuts down. Both would seem to make it be a poor choice for your use case. – CommonsWare Jun 01 '21 at 12:33
  • What should I use instead? I need something that runs in the background, in its own thread, can start a separate thread and can read intents from a broadcast receiver. – Sebi Jun 01 '21 at 12:41
  • "What should I use instead?" -- perhaps I misread your question, but it sounded like you had working code already. On the whole, I am uncertain why these are two separate apps. And, I am uncertain why you are using an asynchronous IPC mechanism (broadcasts) when it seems like you want a synchronous IPC mechanism (bound service or a `ContentProvider`). "I need something that runs in the background, in its own thread, can start a separate thread and can read intents from a broadcast receiver" -- a regular `Service` can do those things, at least for some definition of "read intents". – CommonsWare Jun 01 '21 at 12:48
  • One app (app A) is intended as an extension of the other (B). B can run without A, but A can't without B (business model). A synchronous mechanism would freeze A. But how does a `Service` react to multiple intents? It has only a `startService()` that gets the intent (and starts the service). But I'm not sure about the behavior of `startService()` when called multiple times. Does a new `Service` object get created? or is the current instance kept? `Service` does not have an `onHandleIntent()` that would look more appropriate. – Sebi Jun 01 '21 at 13:02
  • "A synchronous mechanism would freeze A" -- Web servers have a synchronous protocol with respect to Web clients. So, freezing a thread of A is the point. No matter how you slice it, you need to freeze a thread of A. And that is what your current code seems to do, with `sleepThread(3)`. "But how does a Service react to multiple intents?" -- that seems like a non sequitir. You asked earlier for something that "can read intents from a broadcast receiver". A `Service` can call `registerReceiver()` to receive broadcasts, and that is my best interpretation of "read intents from a broadcast receiver" – CommonsWare Jun 01 '21 at 13:06
  • "But I'm not sure about the behavior of startService() when called multiple times. Does a new Service object get created? or is the current instance kept?" -- that depends on whether or not the current service is running. Your service will be called with `onStartCommand()` for each `startService()` call. `onCreate()` will be called every time the service needs to be created, either because it is the first time the service was used in this process or because a previous instance of the service was destroyed (e.g., via `stopService()`). – CommonsWare Jun 01 '21 at 13:08
  • "Service does not have an onHandleIntent() that would look more appropriate." -- `onHandleIntent()` is triggered by a call to `onStartCommand()`. FWIW, [`IntentService` is only ~180 lines of code](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IntentService.java), and most of that is comments. – CommonsWare Jun 01 '21 at 13:10
  • Yes, you are correct about freezing but app B can only use broadcast intents (for now) (`registerReceiver()` looks like the way to go). Can a service be "forced" to run as long as the app is running. To my knowledge there seems to be no set time limit. Oops missed that. – Sebi Jun 01 '21 at 13:17
  • "Can a service be "forced" to run as long as the app is running" -- a service will remain started until either something stops it (`stopService()`, `stopSelf()`), the service crashes, or the process is terminated. If the service is proactively stopped, `onDestroy()` is called, and that is where you would `unregisterReceiver()`. – CommonsWare Jun 01 '21 at 13:20

0 Answers0