02 - Android App
App components
- Logging
- Context
- Activity
- Intent
- Service
- Broadcasts
Logging
Full support in ide
Use android.util.Log
Verbose Log.v()
, debug Log.d()
, info Log.i()
, warn Log.w()
, error Log.e()
, or "what a terrible Failure" Log.wtf()
Deployed application should not contain logging code!
Use BuildConfig.DEBUG flag for checking state (deployed or not)
TAG – string
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = this::class.java.declaringClass!!.simpleName
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "OnCreate")
}
public static int w (String tag, Throwable tr)
public static int w (String tag, String msg, Throwable tr)
Context
- The Context class is an "Interface to global information about an application environment".
- The Context class itself is declared as abstract class, whose implementation is provided by the Android OS. The documentation further provides that Context "…allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc".
- the Context provides the answer to the components question of "where the hell am I in relation to app/system generally and how do I access/communicate with the rest of the app?"
Activity
- Activity – one screen (UI and code)
- User interface – 1..n activities
- Every activity is separate component
- Activity can start other activities
- Back stack (lifo)
- Activity has to be declared in manifest!
Many predefined activities
- Activity
- FragmentActivity
- ListActivity
- PreferenceActivity
- TabActivity
One activity in app is designated as "Main"
Launched on first app activation
Every time new activity is started, previous one is stopped
Previous activity is stored in the back stack
When activity is stopped/paused, callback methods are called
Callbacks – create, resume, stop, destroy, etc…
Create new activity
- Create subclass of Activity (or subclass of subclass of Activity)
- Implement callbacks
- override fun OnCreate(…)
- Implement user interface
- XML layout file
- Or programmatically
- Declaration in AndroidManifest is mandatory
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo2022s01">
<activity
android:name=".SecondActivty"
android:exported="false" />
- Specify intent filters
- Intent filter declares, how other system components may use this activity
- Auto-created stub for main activity
- Action action.MAIN – activity responds to the "main" action
- Category category.LAUNCHER – actitvity is placed into launcher category
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo2022s01">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Start new activity
- startActivity(intent)
- Starting your own activity – specify class name
val intent = Intent(this, OtherActivity::class.java)
startActivity(intent)
- Calling other activities
- Intent.EXTRA_EMAIL – stores list of email recipients
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray)
startActivity(intent)
Start new activity, wait for result
Deprecated, classic way
- StartActivityForResult()
- Implement onActivityResult() callback method
val PICK_CONTACT_REQUEST = 1234
fun pickContact() {
val intent = Intent(
Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI
)
startActivityForResult (intent, PICK_CONTACT_REQUEST)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && requestCode == PICK_CONTACT_REQUEST) {
val cursor = contentResolver.query(
data!!.data!!,
arrayOf(ContactsContract.Contacts.DISPLAY_NAME),
null,
null,
null
)
if (cursor!!.moveToFirst()) {
val columIndex =
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
val name = cursor.getString(columIndex)
Log.d (TAG, "Name: "+name)
}
}
}
Current way
var launchSomeActivity = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
// your operation...
}
}
fun openSomeActivityForResult() {
val intent = Intent(this, SecondActivty::class.java)
launchSomeActivity.launch(intent)
}
Shut down activity
- finish() – activity closes itself
- shut down previously started activity – finishActivity(id)
Activity lifecycle
Three essential states
- Resumed
- In foreground, has user focus. "running"
- Paused
- Activity is partially visible, and is "alive". Can be killed by system in low memory situation
- Stopped
- Activity is 100% obscured by another activity. It is alive, but is not attached to the window manager. Can be killed by system, when memory is needed.
Paused or Stopped – system calls finish() method on activity. When activity is reopened, it must be created again
Lifecycle callbacks
- Fundamental callbacks
- Must always call the superclass implementation before doing any work
class SecondActivty : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// The activity is being created.
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second_activty)
}
override fun onStart() {
super.onStart()
// The activity is about to become visible.
}
override fun onResume() {
super.onResume()
// The activity has become visible (it is now "resumed").
}
override fun onPause() {
super.onPause()
// Another activity is taking focus (this activity is about to be "paused").
}
override fun onStop() {
super.onStop()
// The activity is no longer visible (it is now "stopped")
}
override fun onDestroy() {
super.onDestroy()
// The activity is about to be destroyed.
}
}
Lifecycle flow
SaveInstanceState
- system calls onSaveInstanceState() before making the activity vulnerable to destruction
- Passes Bundle, as name-value pairs
- Save state, using
- putString() and putInt()
- Bundle is passed back in
- onCreate() and onRestoreInstanceState()
Conf changes
Orientation change, physical keyboard, language
System recreates the running activity
- calls onDestroy(),
- then immediately calls onCreate()
Intent and Intent filters
- Messaging object, used for requesting action from another app component
- Fundamental uses
- Start an activity
- startActivity or startActivityForResult
- Start a service
- startService or bindService
- Deliver broadcast
- sendBroadcast, sendOrderedBroadcast, or sendStickyBroadcast
- Start an activity
Explicit intents
- Specify component by name (usually in your own app)
fun startServiceWithExplicitIntent(){
val intent = Intent(this, MyService::class.java)
startService(intent)
}
Implicit intents
- Declare general action to perform
- System searches in manifests (intent-filter) for suitable activity
- If several are found, user is presented with dialog for picking
fun sendTextMessageImplicit() {
// Create the text message with a string
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.putExtra(Intent.EXTRA_TEXT, "Hello!")
sendIntent.type = "text/plain"
// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(packageManager) != null) {
startActivity(sendIntent)
}
}
- Forcing an app chooser
- To show the chooser, create an Intent using
createChooser()
and pass it tostartActivity()
- To show the chooser, create an Intent using
fun sendTextMessageAppChooser() {
// Create the text message with a string
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.putExtra(Intent.EXTRA_TEXT, "Hello!")
sendIntent.type = "text/plain"
val chooserIntent = Intent.createChooser(sendIntent, "Send via")
// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(packageManager) != null) {
startActivity(chooserIntent)
}
}
Intent filter
- To advertise which implicit intents your app can receive
- declare one or more intent filters for each of your app components with an
<intent-filter>
element in your manifest file - Action
- intent action accepted, in the name attribute
- Data
- type of data accepted, using one or more attributes that specify various aspects of the data URI (scheme, host, port, path, etc.) and MIME type
- Category
- category accepted, in the name attribute.
- In order to receive implicit intents, you must include the CATEGORY_DEFAULT
Activity declaration with an intent filter to receive an ACTION_SEND intent when the data type is text
<activity android:name="ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
An implicit intent is tested against a filter by comparing the intent to each of the three elements.
To be delivered to the component, the intent must pass all three tests.
Social app
<activity android:name="ShareActivity">
<!-- This activity handles "SEND" actions with text data -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google.panorama360+jpg" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
Receiving intent on activity launch
Parse and check intent in OnCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second_activty)
if (intent.action == Intent.ACTION_SEND) {
if (intent.type == "text/plain") {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
Log.d(TAG, text)
}
}
}
Service
- Runs in the background without direct interaction with the user
- Not bound to the lifecycle of an activity
- Used for repetitive and potentially long running operations
- Internet downloads
- checking for new data
- Streaming
- GPS/Sensors
- Service runs in the same process as the main thread of the app
- Use asynchronous processing in the service
Paltform services
- Predefined system services
- Application can use them, given the right permissions
getSystemService()
fun buttonOkOnclick(view: android.view.View) {
textViewGreeting.text = "Hello, " + editTextName.text + "!"
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(currentFocus?.windowToken, 0);
}
Custom services
- Declare in manifest
- Inside
<application>
tags!
- Inside
- Extend the Service class or one of its subclasses.
- Start service
- Can also start via
bindService()
. Allows direct communication with the service - Use
android:exported="false"
for keeping service private
class MyService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// TODO: do something useful
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
}
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
- start the service
fun startService() {
val intent = Intent(this, MyService::class.java)
intent.putExtra("KEY1", "Value1")
startService(intent)
}
Service restart
- START_STICKY
- Service is restarted if it gets terminated. Intent data passed to the onStartCommand method is null. Used for services which manages their own state and do not depend on the Intent data.
- START_NOT_STICKY Service is not restarted. Used for services which are periodically triggered anyway.
- START_REDELIVER_INTENT
- Similar to Service.START_STICKY but the original Intent is re-delivered to the onStartCommand method.
Service stop
stopService()
One call to the stopService()
method stops the service.
stopSelf()
– service terminates itself. Used when service finishes its work.
Communicating with service
-
Simple scenario – no direct communication.
Service receives intent when starting. -
Activity binds to local service
IBinder
,onBind()
-
Using receiver
Service broadcasts events
Activity registers broadcast receiver and receives events from service
Service lifecycle
Broadcasts
- Two types of messages – Local (inside your App) or Global
- IntentFilter
- Receiver declared in code or in Manifest
IntentFilter – you can only receive declared broadcasts
private val localReceiverIntentFilter = IntentFilter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "OnCreate")
// whitelist the actions we want to receive
localReceiverIntentFilter.addAction(C.ACTION_TIMEMESSAGE)
localReceiverIntentFilter.addAction(C.ACTION_AUDIO_PLAYING)
localReceiverIntentFilter.addAction(C.ACTION_AUDIO_BUFFERING)
localReceiverIntentFilter.addAction(C.ACTION_AUDIO_STOPPED)
}
Internal broadcast receiver – inside main class (or service)
private inner class BroadcastReceiverInMainActivity : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(
TAG,
"BroadcastReceiverInMainActivity.onReceive " + (intent?.action ?: "null intent")
)
when (intent?.action) {
C.ACTION_AUDIO_BUFFERING -> buttonPlayStop.text = "BUFFERING"
C.ACTION_AUDIO_STOPPED -> buttonPlayStop.text = "PLAY"
C.ACTION_AUDIO_PLAYING -> {
audioStatus = C.AUDIO_PLAYING
buttonPlayStop.text = "STOP"
}
}
}
}
Register and unregister your receiver
Do not double register!
private val localReceiver = BroadcastReceiverInMainActivity()
override fun onResume() {
super.onResume()
LocalBroadcastManager.getInstance(this)
.registerReceiver(localReceiver, localReceiverIntentFilter)
}
override fun onPause() {
super.onPause()
LocalBroadcastManager.getInstance(this).unregisterReceiver(localReceiver)
}
Send out local brodcasts (in service for example)
fun sendBroadcast() {
LocalBroadcastManager.getInstance(applicationContext)
.sendBroadcast(Intent(C.ACTION_AUDIO_BUFFERING))
}
Scheduled tasks
Use scheduledExecutorService
private val scheduledExecutorService = Executors.newScheduledThreadPool(1)
private fun startTimerService() {
scheduledExecutorService.scheduleAtFixedRate(
Runnable {
Log.d(TAG, "scheduledExecutorService running")
val intent = Intent(C.intentBackgroundServiceTime)
intent.putExtra(C.intentBackgroundServiceTimePayload,
Date().toString())
LocalBroadcastManager
.getInstance(applicationContext).sendBroadcast(intent)
},
0, // initialDelay
5, // period
TimeUnit.SECONDS
)
}
override fun onDestroy() {
Log.d(TAG, "onDestroy")
super.onDestroy()
scheduledExecutorService.shutdown()
}