0

I am working on an app that will communicate with a UDP server for its data. Obviously, this is going to be a long-running process (i.e., the life of the activity), so I want to run this process in the background. When the app gets its data from the UDP server, I will, obviously, want to manipulate the data so I can display it within the app's UI.

From what I have learned, there are 3 ways to run a long-running process in the background:

  1. Standard Java Thread
  2. AsyncTask
  3. Service (Bounded)

For the standard Java thread, from my searches on the internet, I hear lots of grumblings on how creating your own threads in Android is not a good idea. I have tried this route before and it does work, but it also requires quite a bit of manipulation in order to use the thread's received data within the app's UI.

For an AsyncTask, the Android documentation itself says:

AsyncTasks should ideally be used for short operations (a few seconds at the most.)

This is not good for me, as I need this thread to run for the entire life of the activity.

Lastly, there is the bound Service. This seems perfect for my needs. Again, from the Android documentation:

A Service is an application component that can perform long-running operations in the background, and it does not provide a user interface. Another application component can start a service, and it continues to run in the background even if the user switches to another application. Additionally, a component can bind to a service to interact with it and even perform interprocess communication (IPC).

After reading this bit, I proceeded down the Android Service path. Things were going well until I tried to run my app.


First, here is my service:

Listener.java:

public class Listener extends Service {

    private boolean keepWorking = true;
    private int localPort;
    private final IBinder iBinder = new LocalBinder();

    public class LocalBinder extends Binder {
        Listener getService(){
            return Listener.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return iBinder;
    }

    public void listenToServer(){
        try {
            byte[] buffer = new byte[16 * 1024];
            DatagramPacket recvPacket = new DatagramPacket(buffer, buffer.length);
            DatagramSocket server = new DatagramSocket(0);
            server.setSoTimeout(1000);
            localPort = server.getLocalPort();

            while (keepWorking) {
                try {
                    server.receive(recvPacket);
                    byte[] data = new byte[recvPacket.getLength()];
                    System.arraycopy(recvPacket.getData(), 0, data, 0, data.length);
                    // Do stuff with the data ...
                } catch ( IOException ioe) {
                    // something here
                }
            }
        } catch (SocketException se) {
            // something here
        }
    }

    public int getPort() {
        return this.localPort;
    }

    public void stopWorking() {
        this.keepWorking = false;
    }
}

And, my MainActivity.java that binds to this service:

public class MainActivity extends AppCompatActivity {

    Listener myListener;
    boolean isBound = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, Listener.class);
        startService(intent);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(isBound){
            unbindService(serviceConnection);
            isBound = false;
        }
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Listener.LocalBinder localBinder = (Listener.LocalBinder) service;
            myListener = localBinder.getService();
            myListener.listenToServer();
            isBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            isBound = false;
        }
    };
}

I quickly discovered that, even though I am doing this from a service, it is apparently still being run from the main thread anyway, as I get this error:

android.os.NetworkOnMainThreadException

from the line in my Listener.java that reads server.receive(recvPacket);.

After digging around, I discovered that I may need a thread or AsyncTask to handle this anyway.

So, my question is this: What is the authoritative, "proper" way to perform long-running, network operations in an Android app?

Why does Android make it so difficult to do what is, really, the "bread and butter" of Android apps? I know I can't be the first person to want to do this.

Thanks for your help.

Brian
  • 1,726
  • 2
  • 24
  • 62
  • "Obviously, this is going to be a long-running process (i.e., the life of the activity)" -- that is not a long-running process in general. Do you anticipate that the user will be using your activity, without touching anything else, for hours/days/weeks? "it is apparently still being run from the main thread anyway" -- of course. You specifically wrote your app that way. You are directly calling a method on the service from the main application thread, then are wondering why that method was called on the main application thread. – CommonsWare May 04 '17 at 15:47
  • If you want to communicate with your server while your activity is started, use a background thread. Whether that is one you fork yourself or you get from something else (e.g., RxJava/RxAndroid) is up to you. Yes, you will need to use something (Rx, event bus, `runOnUiThread()`, `post()`, `Handler`, etc.) to get results from the background thread to the main application thread. Computer programmers usually do not find this to be especially difficult. – CommonsWare May 04 '17 at 15:51
  • @CommonsWare Yes, I fully intend for the users to be running just my activity for hours and maybe days at a time. This is a **very** special use app that just a handful of people in my organization will be using. Although, if successful, it will be pushed to quite a few people in my organization. And, yes, I wrote my app that way - to run from main - but threads are separate processes and I didn't think it could link back to main. – Brian May 04 '17 at 15:54
  • Well, humans need to sleep, and mobile devices will run out of charge. The only way that your activity will be started, without stopping, is if the device's lockscreen never engages and the screen is always on, and that's *very* atypical. So, if the lockscreen engages, and your activity is no longer in the foreground... what happens? If the device powers down the WiFi radio when the screen is off... what happens? If the device enters Doze mode... what happens? If "life of the activity" is the true benchmark, meaning you don't care what happens, then a plain thread is a fine solution. – CommonsWare May 04 '17 at 16:03
  • "but threads are separate processes" -- threads and processes are very distinct concepts in most modern operating systems. Your app only has one process, as your particular approach for setting up the service only works if the service and its client are in the same process. And binding is synchronous, regardless, so if you call a method on a bound service from the main application thread, you get the return value from that method on the main application thread. – CommonsWare May 04 '17 at 16:03

0 Answers0