In Android programming (most likely in any kind of programming), the definitions of terms are very important. This is because each term that seems to have a generic meaning has a specific meaning in programming. Understanding the specific definitions allows a developer to read documentations and develop code more effectively.
An application (hence “app”) is the top level construct. An app:
An app has at least one of the following components: activity, service, content provider and/or broadcast receiver. Each component represents an “entry point” to the app. Each entry point represents a ways for someone, or something, to ask an app to “do something”.
An app is static as a collection of files after it is installed from a single APK file. When an app is requested via one of its components, it comes an app (program) in execution, also known as a process.
The significance of a process is that it defined a memory boundary. Within a process, objects can refer to each other. However, an object cannot refer to another object that is not in the same process. This helps to protect one process from memory corruption by other processes what are possibly malfunctioning or malicious.
Where a process defines memory space, a thread defines a point of execution. A process must have at least one thread associated with it. A thread is what goes through the code (Java bytecode in the case of an Android app) and execute instructions sequentially.
All threads of the same process share the same memory space as far as objects are concerned. However, each thread has its own stack. This means the calling and returning of methods as well as exception handling are thread specific even through they can refer to the same objects.
When an app is started (hence creating a process), a single main thread is associated with the process. This thread is forever (well, almost) stuck in a loop that waits for a request, dispatch (process it) and wait for another request. This main thread is common for all components of an app.
Note that the main thread is also known as the UI thread because this is the only thread that can directly access objects associated with the user interface of a process. Although the memory space is shared among all threads, methods of UI objects specifically check to make sure their methods are invoked only from the UI thread.
Additional threads can be added and be associated with a process. Threads of the same process can be seen as asynchronous and parallel execution points. This means that unless there is explicit synchronization, the actual interleaving of execution of threads is undetermined. Furthermore, there is the illusion of parallelism as threads can execute “at the same time”.
The parallelism of threads can be implemented in hardware due to a multi-core processor (where threads actually execute in parallel on different cores). However, more often, the illusion of parallelism is done via clever operating system tricks like time-sliced and round-robin scheduling.
Loosely defined, threads other than the main/UI thread of a process are categorized into “worker” and “background” threads. A “worker” thread tends to be run for a relatively short amount of time and then ends. A “background” thread, on the other hand, tends to run for a long period of time even though it may utilize scheduling or other blocking mechanisms to pause execution.
An app must specify at least one entry point. There are four types of entry points to an app, the Android developer site calls these app “components” (in other words, a component is an entry point type).
An activity is an entry point that corresponds to a user interface (screen). Most often, this type of entry point is for user interaction.
An service is an entry point that permits communications between a process and other processes.
A service has no UI associated with it. However, a foreground service can create status bar. A user is aware of a foreground service but does not interact with it except for the notification on the status bar.
For process interactions that involve content provision, consider the Provider entry instead.
A receiver, implicitly “broadcast”, is an entry point corresponding to something this broadcast by the system or other processes.
The main difference between a service and a receiver is how the connection is made. With a service, the requester must explicitly identify the recipient of the request. With a receiver, the broadcaster does not know who (if anyone at all) is receiving the broadcast.
As a result, the requester of a service often expects a reply when the requested service is rendered. Compare this to a broadcaster who does not expect any acknowledgements of the broadcast content.
An example of a receiver is one that receives the global action of “airplane mode”. This allows an app that may need to behave differently depending on airplane mode to receive the global action of “airplane mode on” and “airplane mode off”. Upon reception of these actions, the process can determine how to proceed.
A provider, implicitly “content”, is an entry point for requests in the form of an URI for specific content. While a provider seems like a specific case of a service, it inherently supports permission access control and “cursor control”.
“Cursor control” is a mechanism for lengthy content retrieval. For example, a database query can end up with many rows of data. A cursor allows the requester to receive and process one row at a time while suspending and resuming the processing of the request on the provider side.
An example of a provider is the contact provider. A process corresponding to Android Contact has a provider component so that other apps can, when authorized when installed, query contact information from it.
It is important that each class corresponding to each component does not instantiate any threads. Instead, each one instantiate an object that represents the corresponding entry point.
This means that even with objects created for the components, there is still only one main/UI thread associated with a process. The same thread is shared by all entry points.
If a service is, indeed, a background one that is for the most part independent to the other entry points, then a separate thread should be started for the thread.
To make use of services, the first step is to change app/manifests/AndroidManifest.xml to indicate the app offers a service. This can be done by directly editing the XML file. However, to utilize the automatic code generation of Android Studio, the best way is to right click on the app (in Project pane), “new”, then “Service”, then “Service” again. You can, then, edit the new content of the XML file.
Android Studio also automatically generates a class file corresponding to the name of the service. This is where most of the code goes.
Note that if you intend run a foreground service where it has a notification icon, you may also want to add an icon as a image or vector asset (right click “app” in the project pane, “new”, then “image asset” or “vector asset”.
In the class definition corresponding to the service, there are several important point where you can insert custom logic.
The first one is the default constructor of the class. This is called by the the start up code of the main/UI thread. The importance is that the constructor is called by the thread that handles the UI. If the service needs to interact with the UI thread via a Handler object, this is a good time to create one.
Recall that a Handler object is one that links a thread to the message loop of (most likely) another thread. A Handler object instantiated in the constructor of a Service object allows the corresponding service entry point start a new thread later on and be able to interact with the main/UI thread of the process.
The onBind method of Service is for binding a requester to a service via a binder. While this is one of the main reasons to use a service component, binding is a complex mechanism that facilitates IPC (interprocess communication).
In addition to servicing a bind request, a service can also be started by an explicit startService call with an Intent object that specifies the Service subclass. This leads to a call to the onStartCommand method of the Service class, and this method can be overriden (as it is not final) to specify what to do when a service is explicitly requested but not via binding.
If a service is to become a service that has its processing to perform, overriding onStartCommand of a Service subclass is a good place to start a new thread just for the service. To a thread, first create a Thread object with a Runnable parameter specifying what the thread does. If what the new thread does needs access to the UI thread, onStartCommand is also called from the UI thread, and this is a good place to instantiate a Handler object as a way to communicate with the UI thread.
A parallel service thread can specify any blocking calls without interfering with the UI thread because the threads operate in parallel. This means any timing using Thread.sleep or time consuming computations can be done here without worrying about UI response time. A service thread can even be an infinite loop!
Once a Thread object is created, the corresponding thread is not active, yet. The start method of a Thread object start a new thread to run the run method of the parameter used to create the thread. Be sure to call this method, otherwise the Thread object does not do anything.
A foreground service is another common use of a service. A foreground service (not necessarily having a parallel thread of its own) is an entry to a process that is a service (without any user interface of its own) but with notification capabilities.
A Notification object is built by the static class Notification.Builder and its various static methods. Via the methods of Notification.Builder, a Notification object can be created to specify various aspects of a notification.
A notification is a means of interaction via status bar notifications. It specifies the icon (a drawable) to be used to represent a foreground service. It specifies a title and a text area when the notification bar is dragged down. It specifies what to do when the notification is clicked (once the status bar is dragged down). A notification can also specify custom View objects to use.
Once a Notification object is prepared, then a call to the Service method startForeground turns a service into a foreground service. This shows the specified icon on the status bar.
It should be noted that a service is an entry point to a process that is independent to the activities of the same process. This means all activities can be closed and the process can still keep going because at least one service is still active. A foreground service, with its status bar icon, is a useful mechanism to restart activities related to a process so it can interact with a user again. This is done by using setContentIntent when building a Notification object.