Android Application Model II
CSE 5236: Mobile Application Development
Instructor: Adam C. Champion, Ph.D.
Course Coordinator: Dr. Rajiv Ramnath
Reading: Big Nerd Ranch Guide, Chaps. 3, 5 (Activities);
Chap. 28 (Services); Chap. 14 (DB); Chap. 15 (Contacts)
1
Outline
• Activity Lifecycle
• Services
• Persistence
• Content Providers
2
Recap
• Android Framework
• Activities
• General UI:
– Layouts, Handler methods
– Widgets, Custom UI classes, Handling within activity
• Specialized UIs:
– Menus and the Action Bar
– Special Activities – Preferences
• UI for larger screens: Fragments
3
The Activity Lifecycle
• Android runtime manages Activities
• Activities have a “lifecycle” consisting of
states: from creation until death
• Standard (lifecycle) methods on the
activity are invoked at each state change
(can test by rotating device)
4
Activity States
• Created: Born to run
• Active: Working 9 to 5
• Paused: I’m about to break
• Resumed: Back to work
• Stopped: Obscured by clouds, vulnerable
5
Activity Transitions
• Created ⟹ Active
• Active ⟺ Paused
• Paused ⟹ Stopped ⟹ Active
• Stopped ⟹ Killed
6
Timing of
Activity
Lifecycle
Methods
7
Lifecycle Methods
• onCreate(Bundle savedInstanceState): create
views, (re) initialize state
• onStart(): Restore transient state; one-time processing
• onResume(): Session-specific processing, restore
transient state
• onPause(): Save persistent data, release resources,
quickly! Last method guaranteed to be called.
• onStop(): Called optionally by runtime
• onDestroy(): If finish() is called, or object is being
temporarily destroyed. Distinguish via isFinishing().
8
Transient State Management Methods
• onSaveInstanceState(...): Called
before onPause(); use to save transient
state.
• onRestoreInstanceState(...): Called
after onStart() and onResume(); use to
restore state.
9
Transient State Management: Java
// MapsActivity.java
public class MapsActivity extends AppCompatActivity /* . . . */ {
private MapView mMapView;
private String whereAmIString = null;
. . .
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mMapView.onSaveInstanceState(outState);
if (whereAmIString != null) {outState.putString(WHERE_AM_I_STRING, whereAmIString);}
}
. . .
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
whereAmIString = savedInstanceState.getString(WHERE_AM_I_STRING);
if (whereAmIString != null) { mEditLocation.setText(whereAmIString); }
}
10
}
Interaction Across Activities
• Activity 1: onPause()
• Activity 2: onCreate(), onStart(),
onResume()
• Activity 1: onStop() – if it is obscured
11
Starting Activities: Intents and
Intent Filters
• Message posted to the Android runtime to launch
an activity; matched against IntentFilter of
activity in AndroidManifest.xml file
• Encourages activity reuse among applications
• Uniform mechanism for launching internal and
external activities
• Enables loose coupling between caller, responder
12
Intent and Intent Filter Components
• Target – fully qualified name (string),
direct reference
• Action – standard or custom (string)
• Data (URI)
• Category of the target object
13
Intent Filters: Examples
<!-- AndroidManifest.xml -->
<intent-filter> <!== for SplashScreen activity ==>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter> <!== for Login activity ==>
<action
android:name="com.wiley.fordummies.androidsdk.tictactoe.Login">
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
14
Intent Invocations: Examples: Java
• Matching component name: startActivity(new Intent(
"com.wiley.fordummies.androidsdk.tictactoe.Login"));
• Direct invocation: startActivity(new Intent(
getActivity().getApplicationContext(), SettingsActivity.class));
• Passing data: public void sendScoresViaEmail() {
Intent emailIntent = new Intent(Intent.ACTION_SEND);
emailIntent.putExtra(Intent.EXTRA_SUBJECT,
"Look at my AWESOME TicTacToe Score!");
emailIntent.setType("plain/text");
emailIntent.putExtra(Intent.EXTRA_TEXT, firstPlayerName +
" score is " + scorePlayerOne + " and " +
secondPlayerName + " score is " + scorePlayerTwo);
startActivity(emailIntent); 15
Intent Invocations: Examples: Kotlin
• Matching component name: startActivity(Intent(
"com.wiley.fordummies.androidsdk.tictactoe.Login"))
• Direct invocation: startActivity(Intent(activity.applicationContext,
SettingsActivity::class.java))
• Passing data: fun sendScoresViaEmail() {
val emailIntent = Intent(Intent.ACTION_SEND)
emailIntent.putExtra(Intent.EXTRA_SUBJECT,
"Look at my AWESOME TicTacToe Score!")
emailIntent.type = "plain/text"
emailIntent.putExtra(Intent.EXTRA_TEXT, mFirstPlayerName +
" score is " + mScorePlayerOne + " and " + mSecondPlayerName +
" score is " + mScorePlayerTwo)+-
startActivity(emailIntent)
16
}
Tasks and Activity Stacks
• Task: a (conceptual) container for sets of “like-
minded” activities
• Roughly corresponds to a Linux process
• Activities are stacked in tasks
• Activities can move across tasks
• Task is requested by calling intent or selected
based on taskaffinity attribute in
AndroidManifest.xml file
17
Outline
• Activity Lifecycle
• Services
• Persistence
• Content Providers
18
Services
• Activities for background, long-term processing (e.g., email,
music, synchronization)
• Local: Accessible by a single app.
• Remote (aka bound): Accessible by all apps on the device
– See: http://developer.android.com/guide/topics/fundamentals/
services.html#CreatingBoundService
• Comparison with threads:
– Coarser-grained processing
– Lifecycle separated from launching activity
– More resources consumed
– More respected by OS
19
Service Example: Java
public class MediaPlaybackService extends Service {
MediaPlayer player;
@Override
public IBinder onBind(Intent intent) {/*...*/}
public void onCreate() {
player = MediaPlayer.create(this, R.raw.sampleaudio); player.setLooping(true);
}
public int onStartCommand(Intent intent, int flags, int startId) { /* ... */
Bundle extras = intent.getExtras();
String audioFileURIString = extras.getString("URIString");
Uri audioFileURI = Uri.parse(audioFileURIString);
player.reset(); player.setDataSource(this.getApplicationContext(),
audioFileURI);
player.prepare(); player.start(); /* ... */
}
public void onDestroy() { player.stop(); }
} 20
Service Example: Kotlin
class MediaPlaybackService : Service() {
override fun onBind(intent: Intent): IBinder? {/*. . .*/}
override fun onCreate() {
player = MediaPlayer.create(this, R.raw.sample_audio)
player.apply { isLooping = true } }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val extras = intent.extras
if (extras != null) {
val audioFileURIString = extras.getString("URIString")
val audioFileURI = Uri.parse(audioFileURIString)
player.reset()
player.setDataSource(this.applicationContext, audioFileURI)
player.prepare()
player.start()
} }
override fun onDestroy() { player.stop() }
}
21
Service Invocation
Java Kotlin
. . . . . .
Intent musicIntent = val musicIntent = Intent(
new Intent( activity.applicationContext,
getApplicationContext(), MediaPlaybackService::
MyPlaybackService.class); class.java)
musicIntent.putExtra( musicIntent.putExtra(
"URIString", "URIString",
mAudioFileURI.toString()); mAudioFileUri.toString())
activity.startService(
getActivity().
musicIntent)
startService(musicIntent);
. . .
. . .
22
Outline
• Activity Lifecycle
• Services
• Persistence
• Content Providers
23
Saving Persistent Data – Files
• Same as in any Java or Kotlin app
• Internal storage (Linux file system)
– Local to app
– Destroyed upon uninstall
• External storage (e.g., SD card)
– Stored to SD card or flash memory (device-dependent)
– Shared among applications
– E.g.:
Java Kotlin
File imageFile = val imageFile =
new File(imageFilePath); File(imageFilePath)
24
Saving Persistent Data – SQLite
• http://sqlite.org: “Most widely deployed
SQL database engine in the world”
– Lightweight, transactional
• Helper class for database creation
• Methods for operating on SQL statements:
– Creating a statement
– Binding data
– Executing
25
SQLite Example: Account Model Class
Java Kotlin
// Account.java // Account.kt
public class Account { data class Account(val name: String,
private String mName, mPassword; val password: String) {
// We don't need anything here!
public Account(String name,
}
String password) {mName = name;
mPassword = password; }
public String getName() { . . . } Kotlin data classes automatically
public String getPassword() { ... } generate getters, setters,
equals(), hashCode(),
/* equals(), hashCode(), toString() */
} and toString()
26
SQLite Example: DB Schema: Java
// AccountDbSchema.java
public class AccountDbSchema {
public static final class AccountsTable {
public static final String NAME = "accounts";
public static final class Cols {
public static final String NAME = "name";
public static final String PASSWORD = "password";
}
}
}
// Also used with Kotlin
27
SQLite Example: Account CursorWrapper
Java Kotlin
// AccountCursorWrapper.java // AccountCursorWrapper.kt
public class AccountCursorWrapper class AccountCursorWrapper(cursor: Cursor) :
extends CursorWrapper { CursorWrapper(cursor) {
public AccountCursorWrapper(
Cursor cursor) { super(cursor); } val account: Account
public Account getAccount() { get() {
String name = getString( val name = getString(
getColumnIndex( getColumnIndex(
AccountsTable.Cols.NAME)); AccountsTable.Cols.NAME))
String password = getString(
getColumnIndex( val password = getString(
getColumnIndex(
AccountsTable.Cols.PASSWORD));
AccountsTable.Cols.PASSWORD))
Account account =
new Account(name, password);
return Account(name, password)
}
return account;
}
} 28
}
SQLite Example: Account Singleton: Java
// AccountSingleton.java
public class AccountSingleton { // Continued . . .
private static AccountSingleton sAccount;
public void addAccount(Account account) {
private AccountDbHelper mDbHelper;
ContentValues contentValues =
private SQLiteDatabase mDatabase;
getContentValues(account);
private static final String INSERT_STMT =
. . .
"INSERT INTO " + AccountsTable.NAME +
SQLiteStatement statement =
"(name, password) VALUES (?, ?)" ; /* . . . */
mDatabase.compileStatement(INSERT_STMT);
private AccountSingleton(Context context) {
statement.bindString(1,
mDbHelper = new AccountDbHelper(
contentValues.getAsString(
context.getApplicationContext());
AccountsTable.Cols.NAME));
mDatabase = mDbHelper.getWritableDatabase();
statement.bindString(2,
}
contentValues.getAsString(
private static ContentValues getContentValues(
AccountsTable.Cols.PASSWORD));
Account account) {
statement.executeInsert();
ContentValues values = new ContentValues();
}
values.put(AccountsTable.Cols.NAME,
public void deleteAllAccounts() { /* ... */
account.getName());
values.put(AccountsTable.Cols.PASSWORD, mDatabase.delete(AccountsTable.NAME,
account.getPassword()); null, null);
return values; }
}
}
// Also used with Kotlin
// Continued . . .
29
SQLite Example: Helper Class: Java
// AccountDbHelper.java
public class AccountDbHelper extends SQLiteOpenHelper {
private Context mContext;
private static final String DATABASE_NAME = "TicTacToe.db";
private static final int DATABASE_VERSION = 1;
public AccountDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); }
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL("CREATE TABLE " + AccountsTable.NAME + "(" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
AccountsTable.Cols.NAME + " TEXT, " +
AccountsTable.Cols.PASSWORD + " TEXT" + ")");}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, /* ... */) {
Log.w("Example", "Example: upgrading DB; dropping/recreating tables.");
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + AccountsTable.NAME);
onCreate(sqLiteDatabase); } 30
}
SQLite Example: Helper Class: Kotlin
// AccountDbHelper.kt
class AccountDbHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION) {
private lateinit var mContext: Context
override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
sqLiteDatabase.execSQL("CREATE TABLE " + AccountsTable.NAME + "(" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " + AccountsTable.Cols.NAME +
" TEXT, " + AccountsTable.Cols.PASSWORD + " TEXT" + ")")}
override fun onUpgrade(database: SQLiteDatabase, /* ... */) {
Log.w("Example", ”Upgrading DB; dropping/recreating tables.")
database.execSQL("DROP TABLE IF EXISTS " + AccountsTable.NAME)
onCreate(database) }
companion object {
private val DATABASE_NAME = "TicTacToe.db"
private val DATABASE_VERSION = 1 }
}
31
SQLite Example: DB Operations: Java
// AccountFragment.java
private void createAccount() {
FragmentActivity activity = getActivity();
String username = mEtUsername.getText().toString(),
String password = mEtPassword.getText().toString();
String confirm = mEtConfirm.getText().toString();
if (activity != null) {
if ((password.equals(confirm)) && (!username.equals("")) && (!password.equals("")) &&
(!confirm.equals(""))) {
AccountSingleton singleton = AccountSingleton.get(activity.getApplicationContext());
Account account = new Account(username, password);
singleton.addAccount(account);
Toast.makeText(activity.getApplicationContext(), "New record inserted",
Toast.LENGTH_SHORT).show();
} else if ((username.equals("")) || (password.equals("")) || (confirm.equals(""))) {
Toast.makeText(activity.getApplicationContext(), "Missing entry",
Toast.LENGTH_SHORT).show();
} else if (!password.equals(confirm)) {
/* Show error message */
} else { /* Error handling */ }
}
} 32
SQLite Example: DB Operations: Kotlin
// AccountFragment.kt
private fun createAccount() {
val username = mEtUsername.text.toString()
val password = mEtPassword.text.toString()
val confirm = mEtConfirm.text.toString()
if (password == confirm && username != "" && password != "" && confirm != "") {
val singleton = AccountSingleton.get(activity?.applicationContext)
val account = Account(username, password)
singleton.addAccount(account)
Toast.makeText(activity?.applicationContext, "New record inserted",
Toast.LENGTH_SHORT).show()
} else if (username == "" || password == "" || confirm == "") {
Toast.makeText(activity?.applicationContext, "Missing entry",
Toast.LENGTH_SHORT).show()
} else if (password != confirm) {
/* Error handling */
} else { /* Error handling */ }
}
33
Cloud Persistence
• Cloud data stores useful when app users need
to read other users’ written data
• Example: Google’s Firebase
– Non-relational data store (high availability)
– Realtime Database: persists (key, value) data in a
shared JSON tree
– Cloud Firestore: supports more flexible persistence
(document-based approach)
– More info: http://firebase.google.com
34
Outline
• Activity Lifecycle
• Services
• Persistence
• Content Providers
35
Sharing Content – Content Providers
• Standard content providers:
– Contacts, dictionary, media
• Referred to by URI prefix: content://
– Standard providers
• Permission must be requested in
manifest:
<uses-permission android:name=
"android.permission.READ_CONTACTS"/>
• Android 6+: permission for
“dangerous” actions must be
requested at runtime (
https://developer.android.com/
ListView of
training/permissions/requesting.html ) user’s contacts
36
Content Provider Example: Java (1)
// ContentsFragment.java
public class ContactsFragment extends Fragment implements
LoaderManager.LoaderCallbacks<Cursor> {
private ListView mContactsListView;
private static final String[] PROJECTION = { ContactsContract.Contacts._ID,
ContactsContract.Contacts.LOOKUP_KEY, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY };
private final static String[] FROM_COLUMNS = {
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY };
@Override
public void onActivityCreated(. . .) { // First, call super.onActivityCreated()
Activity activity = getActivity();
if (activity != null) {
mContactsListView = activity.findViewById(R.id.contact_list_view);
requestContacts(); } }
private void requestContacts() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!hasReadContactPermission()) {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
PERMISSION_REQUEST_READ_CONTACTS); }
else { showContacts(); } }
37
else { showContacts(); } }
Content Provider Example: Java (2)
// ContactsFragment.java, continued . . .
private boolean hasReadContactPermission() {
Activity activity = getActivity();
return activity != null &&
activity.checkSelfPermission(Manifest.permission.READ_CONTACTS) ==
PackageManager.PERMISSION_GRANTED; }
@Override
public void onRequestPermissionsResult(. . . ) {
if (requestCode == PERMISSION_REQUEST_READ_CONTACTS) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { showContacts(); }
else { /* Show error message */} }
}
private void showContacts() {
// Gets a CursorAdapter
mCursorAdapter = new SimpleCursorAdapter(getActivity(), R.layout.list_item_contact,
null, FROM_COLUMNS, TO_IDS, 0);
// Sets the adapter for the ListView
mContactsListView.setAdapter(mCursorAdapter);
// Initializes the loader, which loads the contacts asynchronously
getLoaderManager().initLoader(0, null, this); 38
Content Provider Example: Java (3)
// ContactsFragment.java, continued . . .
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(getActivity(), ContactsContract.Contacts.CONTENT_URI,
PROJECTION, null, null, ContactsContract.Contacts.DISPLAY_NAME +
" COLLATE LOCALIZED ASC” );
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Put the result Cursor in the adapter for the ListView
mCursorAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { mCursorAdapter.swapCursor(null); }
} // End of Fragment
39
Content Provider Example: Kotlin (1)
// ContactsFragment.kt
class ContactsFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
private lateinit var mContactsListView: ListView
companion object { . . .
private val PROJECTION = arrayOf(ContactsContract.Contacts._ID,
ContactsContract.Contacts.LOOKUP_KEY, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
private val PERMISSION_REQUEST_READ_CONTACTS = 1
private val FROM_COLUMNS = arrayOf(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
private val TO_IDS = intArrayOf(R.id.contact_info) }
private lateinit var mCursorAdapter: SimpleCursorAdapter
override fun onActivityCreated(savedInstanceState: Bundle?) {// First, call super...
mContactsListView = activity!!.findViewById(R.id.contact_list_view)
requestContacts()}
private fun requestContacts() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!hasReadContactPermission()) { requestPermissions(
arrayOf(Manifest.permission.READ_CONTACTS), PERMISSION_REQUEST_READ_CONTACTS) }
else { showContacts() }
} else { showContacts() } 40
}
Content Provider Example: Kotlin (2)
// ContactsFragment.kt, continued . . .
private fun hasReadContactPermission(): Boolean {
return activity?.checkSelfPermission(Manifest.permission.READ_CONTACTS) ==
PackageManager.PERMISSION_GRANTED }
override fun onRequestPermissionsResult( . . . ) {
if (requestCode == PERMISSION_REQUEST_READ_CONTACTS) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {showContacts()}}
else { /* Show error message */ }
}
private fun showContacts() {
// Gets a CursorAdapter
mCursorAdapter = SimpleCursorAdapter(activity, R.layout.list_item_contact,
null, FROM_COLUMNS, TO_IDS, 0)
// Sets the adapter for the ListView
mContactsListView.adapter = mCursorAdapter
// Initializes the loader
loaderManager.initLoader(0, null, this)
41
}
Content Provider Example: Kotlin (3)
// ContactsFragment.kt, continued . . .
override fun onCreateLoader(id: Int, args: Bundle): Loader<Cursor> {
return CursorLoader(activity, ContactsContract.Contacts.CONTENT_URI,
PROJECTION, null, null, ContactsContract.Contacts.DISPLAY_NAME +
" COLLATE LOCALIZED ASC")
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor) {
// Put the result Cursor in the adapter for the ListView
mCursorAdapter.swapCursor(data)
}
override fun onLoaderReset(loader: Loader<Cursor>) {
mCursorAdapter.swapCursor(null)
}
} // End of class definition
42
Even More?!
• Yes!
• More widgets, styles and themes
• Maps
• Email, media, telephony
• Sensors
43
Summary
• Activities: “Single screen” of content that user sees
– Can be paused, resumed by users
– Managing Activity lifecycle is crucial!
• Services: Long-running tasks
• Persistence:
– Files (internal, external storage)
– (Local) SQLite database
– Look at https://developer.android.com/guide/topics/
data/data-storage.html for info about Room ORM (easier to use
than SQLiteOpenHelper)
• Content Providers: mechanism for sharing data 44