01. Introduction
Mobility
Mobile-first software design:
- What does this mean?
- What makes mobile software different from desktop software?
- Inherently personal
- Ubiquitous: always with us
- Situated:
- Location-aware
- Understands local context through sensors
- Orientation, cameras, Bluetooth etc.
- Connected
- Wi-Fi, cellular, Bluetooth
- Finite
- Limitations: limited memory, CPU, battery etc.
Mobile is a platform:
A “platform” is a system that can be programmed and therefore customized by outside developers—users—and in that way, adapted to countless needs and niches that the platform’s original developers could not have possibly contemplated, much less had time to accommodate.
Marc Andreeson
Cultural trends:
- Apps are casual
- Apps must assume short, interruptible and unplanned sessions
- Apps are paranoid
- Build to be robust: assume network signal can go out, battery can die, app can be killed at any moment
- Apps are personal
- Phones are an extension of our brain: we offload our memory, computing (e.g. maps) onto the phone
- Apps are social
Android vs iOS:
- Apps: commercially, mostly need to have support for both iOS and Android.
- For the course, Android is chosen because as the development tools are open source, allowing both Windows and macOS devices to be used for Android development.
The course
First term (6 weeks) - basics of Android required to build any app.
Second term: location, camera, sensors etc.
Assignment 1: 30%, due 5pm 24 August. Individual assignment.
Assignment 2: 30%, due 5pm 19 October. Group assignment (3 people).
Final exam: 40%.
Not covering cross-platform languages/frameworks: will always lag behind what the native implementations can offer.
Introduction to Android
Android is a software stack for mobile devices, including:
- An operating system
- Middleware
- Key applications
Linux is used to provide core system services:
- Security
- Memory management
- Process management
- Power management
- Hardware drivers
Android versioning:
- ~30 versions over 12 years
- Now about one major release per year
- Android version number != API level
Android Project Structure
manifests/AndroidManifest.xml:
- XML file
- Acts like a table of contents
- Register main activity - starting point for the app
- Declares target, min SDK
- Declares all components of the app: activities, services
- Request permissions
Source code:
- In
/javadirectory, even for Kotlin projects (/srcin the file system) androidTest: for unit tests using the Android emulatortestfor Android-independent tests
Resource file:
/resdirectorydrawable: imageslayout: XML description of view layoutsmenu: XML menu specificationsvalues: strings, dimensions, colors, lists of dataraw: other files (e.g. audio, video)xml: other general-purpose XML files
Gradle:
- Build tool
- Need to configure to pull in external libraries (e.g. Jetpack packages) into the binary
Android Runtime (ART, formerly Dalvik VM):
- Subset of Java developed by Google, optimized for mobile
- Does ahead-of-time compilation (AOT)
- Runs
.dexbytecode files compiled from.classfiles - Creates compiled extended and linkable format (ELF) executables
- Does not support some Java libraries (e.g. AWT, Swing)
Boxing:
- Each app run in its own Linux process
- Can create additional threads for long-running operations
- Each app assigned a unique Linux ID
- App can only access its own files
Packaging:
- Java code compiled by
javacinto bytecode, Kotlin code bykotlinc - Those class files, plus those from external libraries, are compiled into
.dexfiles by `dx - Android Manifest file, resources, and the
dexbytecode packaged into a.apkfile byaapt
Android Debug Bridge (adb):
- CLI tool to interact with emulator or Android device
02. Introduction to Kotlin
Designed by IntelliJ (first released 2011) as a modern replacement to Java, with a less verbose syntax and with support for a more functional style of programming (immutability, higher-order functions, and type inference).
In 2017, Google declared Kotlin an official language for Android development.
@file:JvmName("Main")
// Name of the Java class as the function below is in global scope
fun main(args: Array<String>) {
println("Hello, World!")
}
Variables:
var name: String = "Adam"- Use
valfor constants
- Use
Types:
Double,Float,Long,Int,Short,Byte,Char,Boolean- Type can be inferred from context
- Type conversions must be explicit
- e.g.
someInt.toLong() - When declaring a float, append the value with an
fe.g.val zero: Float = 0f
- e.g.
String templating:
"${someExpression}"syntax for templating"$someVariable"shorthand when referring to variables
Collections:
Array: fixed size, mutableList: fixed size, immutableMutableList: variable size, mutable- Create inline array using
arrayOf('a', 'b', 'c')
Conditional statements
- Single-line statements can be used like a ternary statement
if someCondition someValue else otherValue- Can use braces for multi-line statements
- Value of the last statement in the block is used as the return value of block (the value ‘yielded’ by the block)
Switch:
-
val numDays = when (month) { 1, 3, 5, 7, 8, 10, 12, -> 31 2 -> 28 else -> 30 }``` -
val quadrant = when { x > 0 && y > 0 -> "I" x <= 0 && y > 0 -> "II" x <= 0 && y <= 0 -> "III" x > 0 && y <= 0 -> "IV" else -> "None" }``` - Can also use
in min..maxfor number ranges
Functions:
fun name(param1: Type, param2: Type): ReturnType {
}
fun singleLine() = Random.nextInt(100)
Classes:
class Point(x: Float, y: Float) {
var x: Float = x
var y: Float = y
// Can also use `val` for constants
}
// Same variable name used in constructor argument and
// property name, so can simply exclude property definition
class Point(var x: Float, var y: Float) {
}
class Point(var x: Float, var y: Float) {
override fun toString = "($x, $y)"
}
Getters and setters:
// Within a class block
val computedProperty: Int
get() {
return ...
}
var name: String
set(value) {
println("Name changed from '$field' to '$value'")
field = value
// if `name` is used instead of `field`, it would cause an infinite loop
}
Lambdas:
{ param1: Type, param2: Type ->
body
}
// If lambda is the last parameter, Swift-like syntax to put it outside the braces:
intArrayOf(3, 1, 4, 1).forEach { x -> println(x) }
03. Activities and Layout
App Manifest
Declares several app components:
Activities:
-
The most fundamental component of the Android application model
-
Roughly corresponds to a screen-ful of user interaction.
-
An entry point for user interaction into the app
- Instead of a
mainmethod, there is a main activity declared in the manifest
- Instead of a
-
Subclasses of the
Activityclass- Or a specialized type like
AppCompatActivityorComponentActivity
- Or a specialized type like
-
Registration in app manifest:
<manifest ...> <application ...> ... <activity android:name=".NameOfActivityClass" android:exported="true"> <intent-filter> <!-- Register the activity as the one that executes on app launch --> <action android:name="android.intent.action.MAIN"/> <!-- Make the activity show up as an entry in the app launcher --> <category android:name="android.intent.category.LAUNCHER"/>` </intent-filter> </activity> </application> </manifest>
Fragments:
- Re-usable pieces of UI that are owned by an activity.
Services:
- Application component that performs long-running operations in the background
Content providers:
- Bridge between applications to share data
- Mostly built into the system; you do not usually create new ones yourself
Broadcast receivers:
- Components that responds to system-wide announcement
- e.g. battery low, screen off
- It is also possible to initiate broadcasts from within an application
Activity
Activity stack:
- Navigation represented as a stack of activities, with the current activity on top
- When going back, the top activity is popped off the stack and destroyed
- Beware of creating and pushed multiple instances of the same activity to the stack rather than popping to the existing instance
Lifecycle
┌──────────────► onCreate
│ │
│ │
│ ▼
│ onStart ◄──────────onRestart
│ │ ▲
│ │ │
│ ▼ │
│ onResume ◄───────┐ │
│ │ │ │
│ ▼ │ │
│ ┌────────────────┐ │ │
System │ Activity │ │ │
kills process │ running │ │ │
▲ └──────┬─────────┘ │ │
│ │ │ │
│ Different activity │ │
│ enters foreground │ │
│ │ │ │
│ │ │ │
│ ▼ user returns │
│ onPause──────────┘ │
│ | to activity │
│ Activity no │
│ longer visible │
│ │ │
│ low memory ▼ user navigates │
└─────────────── onStop───────────────────┘
│ to activity
│
Finished/destroyed
│
▼
onDestroy
onCreate:- Occurs on app launch, or if Android kills the process to free memory and the user returns to the app
onStart- An activity is stopped (
onStopped) if the activity is no longer visible. - If the user navigates back to the activity, it may be restarted (
onRestart)
- An activity is stopped (
onResume- A activity is paused (
onPaused) if another activity comes into the foreground - The activity may be partially obscured by another activity and interactions may be disabled
- When the user returns to the activity
- A activity is paused (
Primary states:
- Active:
- Activity is in the foreground, and user can interact with it
- Paused:
- Activity is partially obscured, user cannot interact with it (e.g. when menu/dialog open)
- Stopped:
- Completely hidden and not visible to the user
- Instance and variables retained, but no code is executed
- Examples of scenarios when an activity is stopped:
- User receives a phone call
- User performs an action that starts another activity in the application
- Phone rotation: activity (and the whole UI) is stopped and re-created
Lifetimes:
- Entire lifetime (
onCreate/onDestroy):- Load the UI
- Start and stop threads that should always be running
- Visible lifetime (
onStart/onStop)- Access or release resources that influence the UI
- Write to files if necessary
- Write information to files if necessary
- Foreground lifetime (
onResume/onPause)- Restore state and save state
- Access/release
- Broadcast receivers
- Sensors (e.g. GPS, camera)
- Start/stop video playback
- Start/stop animations
Programmatically stopping an activity:
- Generally shouldn’t be done: let Android manage it for you
- Call when you absolutely do not want the user to return to the instance of the activity
- Call the
finishorfinishActivitymethods
Architecture
Previously, the recommended architecture was a multiple activity architecture.
However, after the Navigation component Jetpack library was introduced in 2018, Google started to recommend a single activity-multiple fragment architecture.
This:
- Simplifies data sharing between different screens in an application using a
SharedViewModel - Simplifies the manifest file
Architectural principles:
- Separation of concerns: modularity, encapsulation
- Model-view design:
- Persistent model independent of views
- Allows multiple views of the same model (e.g. for different screen sizes)
- Each component has its own lifecycle
Views, View Groups and Layouts
- View: visible, interactive UI element
- ViewGroup: invisible container used to hierarchically and spatially organize views and view groups
- Usually called Layouts and implement defined layout structures (e.g.
LinearLayout,ConstraintLayout)
- Usually called Layouts and implement defined layout structures (e.g.
- Layouts can be declared as XML resources:
- Enables separation of presentation from behavior
- Allows different layouts to be created for different device sizes
XML
- Set activity view using
setContentView(R.layout.name_of_xml_layout_file), whereRis a file that has been automatically generated - Get a reference to an element using
findViewById(R.id.element_id)(where the XML element has theandroid:id="@=id/element_id"property)
Tasks:
- Use
Runnablecallbacks - Call
Handler(Looper.getMainLooper())to get a handle to the main thread - Use
handler.postorpostDelayedto execute the given runnable immediately or after some delay - Use
handler.removeCallbacksto remove the runnable (e.g. afteronStopis called)
Misc:
- Use the
lateinitmodifier on a property declaration to specify that you will always set the value before accessing it- Equivalent to ‘var varName!’ in Swift
- Adapter: data source for an array (or other data structure) that is used to populate views. Can be configured to take advantage of view recycling, click events etc.
04. Introduction to Jetpack Compose
Widgets defined in XML, but state change (and ownership), event handling etc. happens in the code, leading to poor separation of concerns.
Jetpack Compose:
- Complete rewrite of Android UI toolkit
- Unbundled from underlying Android OS
- Less boilerplate than views
- State ownership, event handling are more clearly delineated
- Can use Compose within View-based activities
- Often, navigation is often still done with Views (at least for now)
‘Components’ are functions the @Composable annotations:
- Adds a trailing lambda function which allows nesting of Composable functions in a tree
- Kotlin code: can have conditionals, loops etc.
- Compose runtime re-renders the app view when the state changes
05. Intents and Navigation
The three core components of applications: activates, services, and broadcast receivers, are all started through intents.
Intents can be used as a messaging system to activate components in the same application, or to start other applications.
Intents can be implicit or explicit:
- Implicit intent:
- Request for a service
- Any appropriate app on the device can fulfill the request
- If multiple activities are available (and none set as the default), the system will ask the user to pick one
- Explicit intent:
- Start a specific named
Activity
- Start a specific named
Intents can return values to the activity which started it.
Some common intents: alarm clock, calendar, camera, contacts, app, email, file storage, maps, music, phone, search, settings.
TODO
startActivity
startActivityForResult
startService
bindService
Android manifest:
-
Describes components:
- Activities
- Services
- Broadcast receivers
- Content providers
- Intent messages each component can handle TODO
-
Action categories:
- TODO
- e.g.
ACTION_CALL,ACTION_SEARCH
-
Broadcast actions:
- Notification of some event
- e.g.
ACTION_TIMEZONE_CHANGED,ACTION_POWER_CREATED
Context.startActivity()
Activity.startActivityForResult() // expect a return value
Activity.setResult() // Set the return value to give to activtity that started the activity
Context.startService()
Context.bindService()
TODO
Intent object properties:
- Component name
- Fully-qualified class name
- Set using
setComponent,setClass, orsetClassName - Optional (implicit intent)
- Action (e.g.
ACTION_CALL)- Required for implicit intents
- Many actions defined in the
Intentsclass - Other actions defined through the API (e.g.
MediaStoreclass hasACTION_IMAGE_CAPTURE) - Can define your own intent action names to allow other applications to call into your app
- Set using
setAction
- Category (of action)
- TODO
- Data
- URI (uniform resource identifier) of data
- Can set MIME type of the data/content
- Type (of data)
- Extras (Bundle with more data)
- Key-value dictionary
- e.g.
ACTION_TIMEZONE_CHANGEDhas an extra with the keytime-zone
- Flags
- TODO
If your multi-screen app is a multiple-activity app, navigation can be achieved by using explicit intents TODO.
Navigation Architecture:
- XML:
- Navigation graph
- Destinations are pages of your apps
- Actions are edges between the nodes, denoting logical relationship between destinations
- Nested graphs are possible
NavHost- Empty container in layout which holds a destination
- Destinations are swapped in and out
- Default implementation:
NavHostFragment
- Deep links
- Navigation graph
- Compose
NavHostcomposable defines set of routesNavControllerholds current navigation state, allows programmatic navigation to different routes based on events- Argument placeholders allow parameters to be passed to screens
- Navigation with Fragments and Compose:
androidx.compose.ui.platform.ComposeViewcan be added to any XML layout- The view element implements
setContent, allowing you to add Composable functionsbinding.composeView.setContent { ... }
- Allows you to use ML for navigation but do everything else using Compose
Recycler List Views
Generating views is expensive: list views only render the items that will be visible on the screen.
A recycler view is used to manage some backing data set and to re-use existing views as rows become hidden and visible.
data DataItem(val id: Int, val text: String)
class MainActivity: AppCompatActivity(), OnDataItemListener {
private val data = arrayOf<DataItem>(
DataItem(1, "Hello"),
DataItem(2, "World"),
DataItem(3, "Cat")
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dataAdapter = DataAdapter(data)
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.adapter = dataAdapter
}
fun onItemClick(position: Int) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, data[position])
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
}
class DataAdapter(private val data: Array<DataItem>,
private val listener: OnDataItemListener) :
RecyclerView.Adapter<DataAdapter.DataItemViewHolder>() {
// Make the class a subclass of RecyclerView, and call the superclass's
// initializer with the `itemView` as the argument.
// Also pass through the listener to each list item view holder
private class DataItemViewHolder(itemView: View,
private val listener: OnDataItemListener) :
RecyclerView.ViewHolder(itemView) {
val textView: TextView
init {
textView = itemView.findViewById("data_text")
}
override fun onClick(view: View?) {
listener.onItemClick(adapterPosition)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataItemViewHolder {
// parent context required as views need an owner
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, attachToRoot: false)
// list_item.xml is a XML layout resource file for the list item
return DataItemViewHolder(view, listener);
}
// Set view item content depending on element index
override fun onBindViewHolder(viewHolder: DataItemViewHolder, position: Int) {
viewHolder.textView = data[position].text;
}
override fun getItemCount() = data.size
interface OnDataItemListener {
fun onItemClick(position: Int)
}
}
<!-- activity_main.xml -->
...
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
app:layoutManager=:LinearLayoutManager"
/>
<!-- LinearLayoutManager = list view. grid view managers also available -->
...
<!-- list_item.xml -->
...
<TextView
android:id="@+id/data_text"
android:text="Default Text"
/>
...
06. Persisting Data
Options:
- Internal storage:
- Small, private to the app
- Files deleted when app removed
- Used by
SharedPreferences,DataStore(newer) which can be used as a lightweight key-value data store
- External/shared storage
- Large data files
- Possibly shared with other apps
- Files persist after app uninstalled
- SQLite database
- The cloud
Bundles
On rotation, an activity is recreated and hence, we must ensure that state is restored. We use Bundles to transfer information between activities, or between instances of the same activity.
/// Called after `onStart` if a bundle exists
/// Same parameter passed in `onCreate`, but null checking is required, and it
/// may be more convenient to do the state restoration after all initialization
/// has been done or to allow subclasses to override it
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
savedInstanceState?.run {
someStateValue = getString(SOME_KEY)
}
}
override fun onSaveInstanceState(outState: Bundle?) {
savedInstanceState?.run {
putString(SOME_KEY, someStateValue)
}
}
Internal Storage
We can write to a private (not accessible to other apps) internal storage directory using Context.MODE_PRIVATE.
In Android 10 and higher, the directory location is encrypted. The security API can be used to encrypt files in older versions.
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(fileContents.toByteArray())
}
To get the absolute path to the directory, use context.getFilesDir().
context.getDir can be used to get (and if necessary, create) a directory.
context.fileList() returns an array of strings with files associated with the context.
Static Files
Read-only static data which is generated at or before compile time:
- Put files in the project
/res/raw/directory - Use
context.openRawResource(R.raw.raw_file_id)to get anInputStream
Cache files
Use context.getCacheDir() to get a File object which is a reference to a directory where you can create and save temporary files.
The Android system may delete them later if space is needed. However, you should still clean up cache files on yourself (context.deleteFile(name: String)).
SharedPreferences
A key-value store backed by an XML file in the internal storage directory.
Jetpack DataStore
Co-routines used to store data asynchronously. While the Preference DataStore is similar to SharedPreferences, the Proto DataStore can be used to store data as custom data types (and hence, it can perform type-checking).
ExternalStorage
getExternalFilesDir(type: String?) returns the primary shared/external data storage device.
There is no security - any app with the WRITE_EXTERNAL_STORAGE permission can write and overwrite.
The type can be null to get the root directory, or Environment.DIRECTORY_$type where type is something such as DCIM, DOWNLOADS, or SCREENSHOTS.
Shareable Storage Directories
For media content not owned by your app which you wish to be accessible through the MediaStore API.
ORM
Object-Relational Mapping (ORM):
Automatic mapping of models in code to relational database tables. This is found in various platforms:
| Platform | Main ORM implementation |
|---|---|
| Android | Room |
| Ruby on Rails | ActiveRecord |
| Java | Hibernate |
| iOS | CoreData |
Room
Process:
- Model types are annotated (e.g. with the
@Entitytype attribute) - Create a DAO object that bridges between the database and host language types
- Create an interface for the database (getters for each model’s DAO)
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// For Kotlin extension and coroutine support
implementation "androidx.room:room-ktx:$room_version"
Example:
@Entity
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val profileImage: Bitmap?,
)
@Dao
interface Dao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE id IN (:userIds)")
fun getAllWithId(userIds: IntArray): List<User>
@Query("SELECT * FROM user WHERE first_name LIKE :firstName AND " +
"last_name LIKE :lastName LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
val db = Room.databaseBuilder(applicationContext,
AppDatabase::class.java, "database-name").build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
View Models
A helper class which prepares data for the UI. It is lifecycle aware - it is retained during configuration changes (e.g. orientation change), and hence can replace the use of bundles in activities.
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
An activity can have a ViewModel which it uses to share data between fragments:
private val viewModel: MyAppViewModel by viewModels()
The ViewModel can contain LiveData - observable data which can trigger UI updates.
LiveData observables can be mapped to Compose state using androidx.compose.runtime:runtime-livedata and the LiveData.observeAsState() method:
07. Async Tasks and Coroutines
Asynchronous Tasks in Android
The Main/UI thread dispatches events to UI element (widgets). Hence, code running on that threat should always be non-blocking: if it is blocked for more than 5 seconds, it will result in the ‘application not responding’ dialog.
The Android UI toolkit is not thread-safe; that is, it should never be accessed from outside the UI thread.
In Kotlin, Thread and Runnable can be used to create worker threads.
AsyncTask simplifies the creation of workers that interact with the UI.
AsyncTask
Usage:
- Create a worker by subclassing
AsyncTask - Implement the
doInBackground()callback function - Implement
onPostExecute(), which delivers the result to the UI (runs on the UI thread) - Run the task by calling
execute()from the UI thread
You can optionally implement the onProgressUpdate method to provide progress
updates (e.g. progress bars) in the UI.
Kotlin coroutines
Starting with version 1.3, the Kotlin language provides coroutines which
will replace AsyncTask in future Android versions.
Coroutines manage long-running tasks and can safely call network or disk operations.
Coroutines are lighter than threads (use less memory).
Dispatchers:
Dispatchers.Main:- Runs on the Main/UI thread
- Use for:
- Calling suspend functions
- Calling UI functions
- Updating LiveData
Dispatchers.IO:- Thread for performing disk or network I/O outside the main thread:
- Use for:
- Database access
- Reading/writing files
- Networks
Dispatchers.Default:- For CPU-incentive work outside of the main thread (e.g. parsing JSON)
suspend functions:
- Functions that do long-running computations without blocking
- Can only be called from other
suspendfunctions- Other
suspendfunctions can be called like normal methods: synchronously
- Other
- Runs on
Dispatchers.Mainby default withContext(Dispatcher)can be used to change the dispatcher it runs on
Calling suspend functions:
suspend func suspendMethodCall() -> Int {
return withContext(Dispatchers.IO) {
// some kind of synchronous work
10 // Last statement is the return value
}
}
// async
coroutineScope {
val deferred = async { suspendMethodCall() }
val result = deferred.await()
}
// launch
viewModelScope.launch {
suspendMethodCall()
}
// no return value when using launch
Built-in coroutine scopes:
ViewModelScope:- defined for each
ViewModelobject - Cancelled when the
ViewModelis cleared
- defined for each
LifecycleScopeLifecycleholds lifecycle state for a component (e.g. activity, fragment)- Cancelled when the
Lifecycleis destroyed
LiveData
Flows
Flows emit multiple values sequentially, compared to suspend functions which
can only return a single value.
Actors:
- Producer: is usually a data source or repository (e.g. database, socket).
- Consumer: usually the view
- Intermediaries: optional actors which transform the emitted values before arriving
to the consumer (e.g.
map,filter)
Flow builder notation:
fun counter(): Flow<Int> = flow {
// flow builder
for (i in 1..3) {
delay(100); // doing useful work here
emit(i)
}
}
counter().collect { value -> println(value) }
Room + Flow + LiveData + ViewModel:
Suspend Room
┌─────> Remote Data Source
View -----> ViewModel -----> Repository ---|
LiveData Flow └─────> Local Data Source
Suspend Room
08. Services and Broadcast Receivers
Services
Services:
- Are long-running background processes which do not have any associated UI
- Continue to run even if the original application is closed
Examples:
- Downloading an app from an app store
- Playing music even when the music player is dismissed
- Maintaining a network connection in a chat app when they are using a different app
Basic:
- No UI components
- Must be declared in the app manifest
- May be private or public
Starting services:
- Manually using the
Context.startService()method- There will be a
onStartCommandcallback
- There will be a
- Another activity binds to a service via IPC using
bindService()- If public, the activity may belong to another app
- There will be
onBindandonUnbindcallbacks
Stopping services:
- By themselves when the task is completed, using
stopSelf - By the owning app via a call to
stopService - By the system if it needs to free some RAM
Responsiveness:
- Services run on the main thread of the hosting process
- By default, services do NOT create a separate execution thread
IntentServiceuses a worker thread to handle start requests
Service types:
- Foreground:
- Anything noticeable to the user (e.g. audio playback)
- The WorkManager API can be used to schedule referable, asynchronous tasks
- Background:
- Operations not directly noticed by the user
- The system imposes restrictions on background services
- Bound:
- When an application component binds itself to an existing service
- IPC used to interact with the service
- The service runs as long as one or more applications are bound to it
- It gets destroyed when no more applications are bound to it
Broadcast Receivers
A component that response to system-wide broadcast announcements.
Some broadcasts built-in to Android: screen turned off, battery low, storage low, picture captured, SMS received, SMS sent.
Permissions:
- Android 6.0 (Marshmallow, API level 23) introduced permission changes
- Normal permissions: shown to the user at install time
- Dangerous permissions:
- Must be declared at runtime
Receiving broadcasts:
- Subclass
BroadcastReceiverand implementonReceive - Create an
IntentFilterobject to specify the kinds of broadcasts you want - Register and unregister the receiver during
onResume()andonPause()
During development, broadcasts can be spoofed with adb e.g.
adb shell 'am broadcast -a android.intent.action.BOOT_COMPLETED'
Initiating broadcasts:
- Applications can initiate broadcasts to inform other applications of status or readiness
- TODO
Live Demo
Daily notification to take a picture.
<manifest ...>
<application ...>
<receiver
android:name=".AlarmReceiver"
android:enable="true"
android:exported="true" /* Broadcast scope is global, not within the application */
/>
</application>
</manifest>
fun Bundle.toParamsString() = keySet().map { "$it -> ${get(it)}" }.joinToString("\n")
// When the scheduled alarm event occurs, show a notification to the user
class AlarmReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "${intent.action} with extras ${intent.extras.toBundleString()}")
val intent: PendingIndent = Intent(context, MainActivity::class.java).run {
// Open the main activity
PendingIntent.getActivity(
context,
requestCode = 0,
intent = this,
flags = 0
)
}
val notification = Notification.Builder(
context,
Notification.CATEGORY_REMINDER
).run {
setSmallIcon(R.drawable.camera)
setContentTitle("Notification title")
setContentText("Notification text")
// Run intent on click
setContentIntent(intent)
// Remove the notification on click
setAutoCancel(true)
build()
}
// May need to add `@SuppressLint("ServiceCast")` annotation to the method
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(0, notification);
}
}
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
createNotificationChannel();
}
// Used so that users can disable notifications or set preferences on
// how they are delivered (e.g. deliver silently)
private fun createNotificationChannel() {
val channel = NotificationChannel(
Notification.CATEGORY_ALARM,
"Daily reminders",
NotificationManager.IMPORTANCE_DEFAULT
) {
description = "Send daily reminders"
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
// Also need to schedule the reminder on boot
object Utilities {
// Notifications fire immediately: use AlarmManager to schedule them
fun scheduleReminder(context: Context, hour: Int, minute: Int) {
val today = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
}
val intent = Intent(context, AlarmReceiver::class.java).let {
PendingIntent.getBroadcast(context, 0, it, FLAG_IMMUTABLE)
}
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setInexactRepeating(
AlarmManager.RTC,
today.timeInMillis,
AlarmManager.INTERVAL_DAY,
intent)
}
}
09. Location
Location sources:
- GPS
- ~5-10 m accuracy
- Timing precision in the order of 20 ns
- Requires special/general theories of relativity to be taken into effect
- Cell-ID (cell tower triangulation)
- Wi-Fi network
Android has three methods to obtain location:
- GPS
- Most accurate and most energy intensive
- Only works outdoors
- Network Location Provider: combines Cell-ID and Wi-Fi data
- Passive: piggybacks off other applications
Permissions:
<manifest>
// Foreground: one-off access to location
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"
/>
// If you want fine access, you must also request coarse
// Gives you accses to GPS location
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
/>
// Background: requires permission on API level 29 and higher
<uses-permission
android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
/>
</manifest>
Older approach was easy to get wrong: forgetting to disable it and leading to large battery drain:
- Use
LocationManagerto get access to system location servicesgetSystemService(Context.LOCATION_SERVICE)
- Implement
LocationListenersubclass: overrideonLocationChanged,onStatusChanged,onProviderEnabled,onProviderDisabledmethods
Newer approach uses Google Play Services and provides a higher-level API:
- Provides Fused Location Provider: combines multiple location sources in a unified interface
-
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient.lastLocation.addOnSuccessListener {location: Location? -> // Check to ensure accuracy hasn't dropped significantly (e.g. if phone switched from GPS to network) } - Can also request location requests using
LocationCallback:onLocationResult - Geofencing
- Set entrance and exit events
- Max. 100 geofences per app
- Requires fine location, background location permissions
10. Camera
SENG440 exam:
- Given video of an example app
- In-person in the labs
- Access to API docs, Maven dependencies etc., but not full internet
- Last year: simple game where you pick an answer for some question
Camera API
Essentially an intent to the camera app which takes a photo and returns it.
Requires the camera permission in the app manifest.
// Get thumbnail
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(packageManager)?.also {
// OPTIONAL
// Requres full-size image requires read/write access to external storage.
var photoFile: File = try {
// Probably use `getExternalFilesDir(Environment.DIRECTORY_PICTURES)
createImageFile()
} catch (e: IOException) {
null
}
photoFile?.also {
// File provider: probably `androidx.core.content.FileProvider`
// Must be declared in manifest in `<provider>`
uri = FileProvider.getUriForFile(this, "com.example.android.fileprovider", it)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
}
// END OPTIONAL
startActivityForResult(takePictureIntent, 1)
}
}
...
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
// Only returns a small thumbnail
val thumbnailBitmap = data.extras.get("data") as Bitmap
imageView.setImageBitmap(thumbnailBitmap)
}
}
Video is similar: ACTION_VIDEO_CAPTURE, but intent.data is a URI to the file,
not a thumbnail.
CameraX
For direct control of the camera:
- Create a
Cameraobject (on a non-main thread)- Exception thrown if camera already being used
- Create camera preview implementing
android.view.SurfaceHolder.Callback - Control over camera settings
- Features like face detection, time-lapse video etc.
Two versions available: camera and camera2.
However, you should prefer to use the CameraX support library, which is backwards
compatible with the Camera API and makes it easier to deal with device-specific features.
It supports the ImageAnalysis class which makes it easy to perform computer vision
and machine learning.
// Call when view is created
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraFutureProvider.addListener(Runnable {
// Camera lifecycle now bound to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Surface which is passed the video stream
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind existing use cases - it can only be bound to one view at a time
cameraProvider.unbindAll()
// Can add an an analyzer here
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this)) // Running on main, not UI, thread
MLKit Integration
val imageAnalysis = ImageAnalysis.Builder()
// runs on the previews, not the full resolution frames
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image ->
val rotationDegrees = image.imageInfo.rotationDegrees
// ... analysis code
})
cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageAnalysis, preview)
MLKit contains:
- Vision: text recognition, face detection, post detection, selfie segmentation, barcodes scanning, image labeling, object detection and tracking, digital ink recognition
- Natural language processing
- Language identification, translation, smart replies, entity extraction
Lecture Demo
Face detection: keypoints view (drawn on a canvas) overlaid on top of full screen camera stream.
Using constraint layout with camera preview and dots view both matching parent size.
Camera:
override fun onViewCreated() {
...
cameraExecutor = Executors.newSingleThreadExecutor()
// Update points transform when view size changes
viewFinder.post {
setUpCamera();
}
}
fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
// runnable so that it can run on a different thread
cameraProviderFuture.addListener(Runnable {
cameraProvider = cameraProviderFuture.get()
lensFacing = CameraSelector.LENS_FACING_FRONT
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext())
)
}
fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution
val metrics: WindovMetrics = viewFinder.context.getSystemService(WindowManager::class.jova).currentWindowMetrics
val screenAspectRatio = aspectRatio(metrics.bounds.width(), metrics.bounds.height())
val rotation = vievFinder.display.rotation
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
preview = Preview.Builder()
// Set aspect ratio but auto resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation
.setTargetRotation(rotation)
.build()
// So that the user can take a photo
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// Once again, set aspect ratio but allow CameraX to set resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial rotation - will need to call if this changes
.setTargetRotation(rotation)
.build()
imageAnalayzer = ImageAnalysis.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
.also {
it.setAnalyzer(cameraExecutor, FaceAnalyzer({ pointsList ->
facePointsView.points = pointsList
}, { size ->
// Update transform matrix
// Aspect ratio, mirroring
updateOverlayTransform(facePointsView, size)
}))
}
// Unbind existing use cases
cameraProvider.unbindAll()
try {
preview?.setSurfaceProvider(viewFinder.surfaceProvider)
camera = camearProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageCapture,
imageAnalyzer
)
} catch(e: Exception) {
Log.e(TAG, "Failed to bind camera", e)
}
}
class FaceAnalyzer(private val pointsListListener: PointsListListener,
private val sizeListener: SizeListener): ImageAnalysis.Analyzer {
private var isAnalyzing = AtomicBoolean(false)
private val faceDetector: FaceDetector by Lazy {
val options = FaceDetectorOptions.Builder()
.setContourMode (FaceDetectorOptions.CONTOUR_MODE_ALL)
•build()
FaceDetection.getClient(options)
}
private val successListener = OnSuccessListener<List<Face>> { faces ->
isAnalyzing.set(false)
val points = mutableListOf<PointF>()
for (face in faces) {
val contours = face.getAllContours()
for (contour in contours) {
points += contour.points.map { PointF(it.x, it.y) }
}
}
pointsListListener(points)
}
private val failureListener = OnFailureListener { e ->
isAnalyzing.set(false)
}
override fun analyze(image: ImageProxy) {
if (!isAnalyzing.get()) {
// Analyzer may be to slow to process every frame
isAnalyzing.set(true)
if (image != null && image.image != null) {
val mlKitImage = InputImage.fromMediaImage(
cameraImage,
image.imageInfo.rotationDegrees
)
faceDetector.process(mlKitImage)
.addOnSuccessListener(successListener)
.addOnFailureListener(failureListener)
.addOnCompleteListener { image.close() }
}
}
}
}
11. Sensors
Sensor framework:
- Determine which sensors are available
- What the sensor’s capabilities are
- Acquire raw sensor data
- Register/unregister sensor event listeners
Types:
TYPE_ACCELEROMETER- Acceleration in m/s^2
- Includes gravity
TYPE_AMBIENT_TEMPERATURE- Not common
- Room temperature in Celsius
TYPE_GRAVITY- Software sensor
TYPE_GYROSCOPE- Measures rate of rotation
TYPE_LIGHT- Light level in lux
TYPE_LINEAR_ACCELERATION- Excludes effect of gravity
TYPE_MAGNETIC_FIELD- Ambient geomagnetic field in three axes
- Measured in μT
TYPE_PRESSURE- Ambient air pressure in hPa
TYPE_PROXIMITY- Proximity of object
- Usually binary
TYPE_ROTATION_VECTOR- Orientation of device
Can Sensor capabilities:
- Minimum delay (in μS)
- Power consumption (in mA)
- Max range
- Resolution
Using sensor:
- Obtain
SensorManagerobject - Create
SensorEventListenerto listen toSensorEvents- Two methods
onAccuracyChanged(Sensor sensor, int accuracy)onSensorChanged(SensorEvent event)- When sensor value changing
- Should not do heavy computations in this method (or at least not on the same thread)
- Don’t hold on to the event: it is part of a pool of objects and so the values may be altered
- Two methods
- Register the listener
- Registration if for a specific sensor and sampling period
- The sampling period is a suggestion/hint
- Pre-defined constants:
SENSOR_DELAY_NORMAL,UI,GAME, orFASTEST
- Pre-defined constants:
- And unregister when done (e.g.
onPause)
Sensor coordinate system:
- X-axis: parallel to the x-axis of the screen in the device’s natural orientation (may be landscape for tablets)
- Y-axis: parallel to the y-axis of the screen in the device’s natural orientation
- Z: normal to the screen, positive pointing towards the user