Android 4.0
21. Content Provider - SQLite
This chapter
Content Providers are one of the fundamental building blocks of the Android Platform. We have looked at Activities, Notification and Services, and Intent. None of these have dealt with stored data. Content Provider brings us to the concept of how to deal with data stored in the Android.
So, what is a content provider?
As we already know, Android does not provide storage that all applications share. Instead, Android uses content providers that enable apps to save and retrieve data. It also makes data accessible across applications.
A content provider can be viewed as a wrapper around the data. Android exposes data through a content provider. Several content providers are built into Android for access to data such as videos, images, and contact information, an so on.
When using a content provider, all we really need to know is the provider's base URI. From there, we can run queries and construct a URI to a specific instance.
The URI always starts with content://, indicating it is a content URI instead of a URI to a web resource, http://.
Any application on the device can make use of these URIs to access and manipulate data. As a consequence, content providers play a significant role in sharing data between applications.
There are 4 ways we can store data:
- Preferences
Preferences are used to store user preferences for a single application or across applications in a mobile. They are typically name-value pairs accessible to the context. - Files
Files can be stored directly on to the mobile or to extended storage mediums. They are by default not accessible to applications other than the one that created it. - Database (RDBMS)
Android supports creation of databases based on SQLite. These are again private to the application that created it. - Network
Android provides API support to store data over the network on to a server.
Note that all these are various forms of storing data and most often for a single application. Irrespective of how data is stored, if it has to be shared across applications, we need Content Providers.
No matter how the data is stored, Content Providers give a uniform interface to access the data. Data is exposed as a simple table with rows and columns where row is a record and column is a particular data type with a specific meaning. A row could be about a single person and the columns could be the person's first name, number, address, email, and so on.
Each record is identified by a unique _ID field which is the key to the record. Each content provider exposes a unique URI that identifies its data set uniquely. This URI is equivalent to a table name in a database. The URI consists of various parts: eg: content://com.bogotobogo/android/sample/123 is a unique URI. content:// is a standard prefix. com.bogotobogo/android is the authority, sample is the table name, 123 is the unique _ID.
For the native content providers, these unique URIs are declared as constants in an interface. So, in our program we will be using constants like People.CONTENT_URI which internally translates to content://contacts/people.
Here are a few of the native providers:
Browser CallLog Contacts People Phones Photos Groups MediaStore Audio Albums Artists Genres Playlists Images Thumbnails Video Settings
The top-level items are databases and the lower-level items are tables. So Browser, CallLog, Contacts, MediaStore, and Settings are individual SQlite databases encapsulated as providers. These SQLite databases typically have an extension of .db and are accessible only from the implementation package. Any access outside that package must go through the content provider interface.
Let us look at the onCreate():
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); constantsCursor=managedQuery(Provider.Constants.CONTENT_URI, PROJECTION, null, null, null); ListAdapter adapter= new SimpleCursorAdapter(this, R.layout.row, constantsCursor, new String[] {Provider.Constants.TITLE, Provider.Constants.VALUE}, new int[] {R.id.title, R.id.value}); setListAdapter(adapter); registerForContextMenu(getListView()); }
Given a base URI, we can run a query to return data from the content provider related to that URI. This is done by specifying the columns to return, and the constraints determine which rows to return.
Here we are using the Activity.managedQuery() to create and execute a query against the provided URI. It takes five parameters:
- Contact URI
- Which columns to return, the return that we want to get from the query, in our case, it's an array of string, PROJECTION which is defines as:
private static final String[] PROJECTION = new String[] { Provider.Constants._ID, Provider.Constants.TITLE, Provider.Constants.VALUE};
- Which rows to return, a constraint statement.
- Where clause parameters, an optional set of parameters to bind into the constraint clause.
- Sort clause.
In this example, since our goal of query is to return a list from which the user should choose an item, we use SimpleCursorAdapter. This class bridges between the Cursor and a selection widget like ListView. Feed the Cursor into a SimpleCursorAdapter, hand the adapter off to the widget, then we are set, our widget will show the available options.
So, in the code, after executing the managedQuery() and getting the Cursor, it uses SimpleCursorAdapter
ListAdapter adapter= new SimpleCursorAdapter(this, R.layout.row, constantsCursor, new String[] { Provider.Constants.TITLE, Provider.Constants.VALUE}, new int[] {R.id.title, R.id.value });with the following parameters:
- The activity creating the adapter, PlanetConstants, in this case.
- The identifier for a layout to be used for rendering the list entries, R.layout.row.
- Cursor, constantCursor, in this example.
- The properties to pull out of the cursor and use for configuring the list entry View instances (TITLE and VALUE).
- The corresponding identifiers of TextView widgets in the list entry layout that those properties should go into (R.id.title and R.id.value).
After that, we put the adapter into the ListView.
Content provider also offer the capabilities of adding and removing data.
To insert data into a content provider, we have two options available on the ContentProvider interface which is available through getContentProvider to our activity:
- Use insert() with a collection URI and a ContentValues structure describing the initial set of data to put in the row.
- Use bulkInsert() with a collection URI and an array of ContentValues structures to populate several rows at once.
Let us look at our code where we are adding data.
private void processAdd(DialogWrapper wrapper) { ContentValues values=new ContentValues(2); values.put(Provider.Constants.TITLE, wrapper.getTitle()); values.put(Provider.Constants.VALUE, wrapper.getValue()); getContentResolver() .insert(Provider.Constants.CONTENT_URI, values); constantsCursor.requery(); }
The insert() method returns a URI for us to use for future operations on that new object. In the code above, the passed DialogWrapper provide access to the title and value of the constant.
Since we already have a Cursor for the content provider's contents, we call requery() on that to update the Cursor's contents. This, in turn, will update any SimpleCursorAdapter we may have wrapping the Cursor, and that will update any selection widgets we have.
ContentResolver provides applications access to the content data or model. We can get a handle to a ContentResolver by calling the getContentResolver() method within the Activity. This provides methods to insert, update and delete data.
To delete one or more rows from the content provider, we use delete() method on ContentResolver.
private void processDelete(long rowId) { Uri uri=ContentUris .withAppendedId(Provider.Constants.CONTENT_URI, rowId); getContentResolver().delete(uri, null, null); constantsCursor.requery(); }
No databases are automatically supplied to us by Android. If we want to use SQLite, we'll need to create our own database, and then populate it with our own tables, indexes, and data.
To create and open a database, we subclassing SQLiteOpenHelper. This class wraps up the logic to create and upgrade a database, per our specifications, as needed by our application.
private class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, 1); } @Override public void onCreate(SQLiteDatabase db) { Cursor c=db.rawQuery ("SELECT name FROM sqlite_master WHERE type='table' AND name='constants'", null); try { if (c.getCount()==0) { db.execSQL("CREATE TABLE constants (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL);"); ContentValues cv=new ContentValues(); cv.put(Constants.TITLE, "Gravity, Death Star I"); cv.put(Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I); db.insert("constants", getNullColumnHack(), cv); .......... } } finally { c.close(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { android.util.Log.w("Constants", "Upgrading database, which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS constants"); onCreate(db); } }
Our subclass of SQLiteOpenHelper will need three methods:
- The constructor, chaining upward to the SQLiteOpenHelper constructor. This takes the Context, the name of the database, on optional cursor factory, and an integer representing the version of the database schema we are using.
super(context, DATABASE_NAME, null, 1);
- onCreate(), which passes us a SQLiteDatabase object that we need to populate with tables and initial data.
@Override public void onCreate(SQLiteDatabase db) {
- onUpgrade(), which passes us a SQLiteDatabase object and the old and new version numbers, so we can figure out how best to convert the database from the old schema to the new one.
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
To use our SQLiteOpenHelper subclass, we create an instance and ask it to getReadableDatabase() or getWriteableDatabase(), depending on whether or not we will be changing its contents.
db=(new DatabaseHelper(getContext())).getWritableDatabase();
This will return a SQLiteDatabase instance, which we can then use to query the database or modify its data.
To create our tables and indexes, we'll need to call execSQL() on SQLiteDatabase, providing the Data Definition Language (DDL) statement we wish to apply against the database. In our example, we call execSQL() to create the constants table in the DatabaseHelper on onCreate() method:
db.execSQL("CREATE TABLE constants " + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " title TEXT, value REAL);");
This will create a table, constants, with a primary key column _id that is an autoincremented integer, plus two data columns: title (text) and value REAL. SQLite will automatically create an index for us on our primary key column.
If we do not change our table schemas, we might never drop our tables or indexes, but if we do, just use execSQL() to invoke DROP INDEX and DROP TABLE statement as needed.
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ android.util.Log.w("Constants", "Upgrading database, which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS constants"); onCreate(db); }
Now that we have a database and tables, we want to put some data in them. We have two approaches for doing this.
- Use the execSQL() method works for any SQL that does not return results.
- Use the insert(), update(), and delete() methods on the SQLiteDatabase object. There are builder* methods, in that they break down the SQL statements into discrete chunks, then take those chunks as parameters.
In our example, the insert() from class PlanetConstants looks like this:
private void processAdd(DialogWrapper wrapper) { ContentValues values=new ContentValues(2); values.put(Provider.Constants.TITLE, wrapper.getTitle()); values.put(Provider.Constants.VALUE, wrapper.getValue()); getContentResolver() .insert(Provider.Constants.CONTENT_URI, values); constantsCursor.requery(); }
These methods make use of ContentValues objects.
PlanetConstants.java:
package com.bogotobogo.constants; import android.app.AlertDialog; import android.app.ListActivity; import android.content.ContentUris; import android.content.ContentValues; import android.content.DialogInterface; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ListAdapter; import android.widget.SimpleCursorAdapter; public class PlanetConstants extends ListActivity { private static final int ADD_ID = Menu.FIRST; //private static final int EDIT_ID = Menu.FIRST+1; private static final int DELETE_ID = Menu.FIRST+2; private static final int CLOSE_ID = Menu.FIRST+3; private static final String[] PROJECTION = new String[] { Provider.Constants._ID, Provider.Constants.TITLE, Provider.Constants.VALUE}; private Cursor constantsCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); constantsCursor=managedQuery(Provider.Constants.CONTENT_URI, PROJECTION, null, null, null); ListAdapter adapter= new SimpleCursorAdapter(this, R.layout.row, constantsCursor, new String[] {Provider.Constants.TITLE, Provider.Constants.VALUE}, new int[] {R.id.title, R.id.value}); setListAdapter(adapter); registerForContextMenu(getListView()); } @Override public void onDestroy() { super.onDestroy(); constantsCursor.close(); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, ADD_ID, Menu.NONE, "Add") .setIcon(R.drawable.add) .setAlphabeticShortcut('a'); menu.add(Menu.NONE, CLOSE_ID, Menu.NONE, "Close") .setIcon(R.drawable.eject) .setAlphabeticShortcut('c'); return(super.onCreateOptionsMenu(menu)); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case ADD_ID: add(); return(true); case CLOSE_ID: finish(); return(true); } return(super.onOptionsItemSelected(item)); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { menu.add(Menu.NONE, DELETE_ID, Menu.NONE, "Delete") .setIcon(R.drawable.delete) .setAlphabeticShortcut('d'); } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case DELETE_ID: AdapterView.AdapterContextMenuInfo info= (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); delete(info.id); return(true); } return(super.onOptionsItemSelected(item)); } private void add() { LayoutInflater inflater=LayoutInflater.from(this); View addView=inflater.inflate(R.layout.add_edit, null); final DialogWrapper wrapper=new DialogWrapper(addView); new AlertDialog.Builder(this) .setTitle(R.string.add_title) .setView(addView) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { processAdd(wrapper); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // ignore, just dismiss } }) .show(); } private void delete(final long rowId) { if (rowId>0) { new AlertDialog.Builder(this) .setTitle(R.string.delete_title) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { processDelete(rowId); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // ignore, just dismiss } }) .show(); } } private void processAdd(DialogWrapper wrapper) { ContentValues values=new ContentValues(2); values.put(Provider.Constants.TITLE, wrapper.getTitle()); values.put(Provider.Constants.VALUE, wrapper.getValue()); getContentResolver() .insert(Provider.Constants.CONTENT_URI, values); constantsCursor.requery(); } private void processDelete(long rowId) { Uri uri=ContentUris .withAppendedId(Provider.Constants.CONTENT_URI, rowId); getContentResolver().delete(uri, null, null); constantsCursor.requery(); } class DialogWrapper { EditText titleField=null; EditText valueField=null; View base=null; DialogWrapper(View base) { this.base=base; valueField=(EditText)base.findViewById(R.id.value); } String getTitle() { return(getTitleField().getText().toString()); } float getValue() { return(new Float(getValueField().getText().toString()) .floatValue()); } private EditText getTitleField() { if (titleField==null) { titleField=(EditText)base.findViewById(R.id.title); } return(titleField); } private EditText getValueField() { if (valueField==null) { valueField=(EditText)base.findViewById(R.id.value); } return(valueField); } } }
Provider.java:
package com.bogotobogo.constants; import android.content.*; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.hardware.SensorManager; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import java.util.HashMap; public class Provider extends ContentProvider { private static final String DATABASE_NAME="constants.db"; private static final int CONSTANTS=1; private static final int CONSTANT_ID=2; private static final UriMatcher MATCHER; private static HashMap<String, String> CONSTANTS_LIST_PROJECTION; public static final class Constants implements BaseColumns { public static final Uri CONTENT_URI = Uri.parse("content://com.bogotobogo.android.constants.Provider/constants"); public static final String DEFAULT_SORT_ORDER="title"; public static final String TITLE="title"; public static final String VALUE="value"; } static { MATCHER=new UriMatcher(UriMatcher.NO_MATCH); MATCHER.addURI("com.bogotobogo.android.constants.Provider", "constants", CONSTANTS); MATCHER.addURI("com.bogotobogo.android.constants.Provider", "constants/#", CONSTANT_ID); CONSTANTS_LIST_PROJECTION=new HashMap<String, String>(); CONSTANTS_LIST_PROJECTION.put(Provider.Constants._ID, Provider.Constants._ID); CONSTANTS_LIST_PROJECTION.put(Provider.Constants.TITLE, Provider.Constants.TITLE); CONSTANTS_LIST_PROJECTION.put(Provider.Constants.VALUE, Provider.Constants.VALUE); } public String getDbName() { return(DATABASE_NAME); } public int getDbVersion() { return(1); } private class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, 1); } @Override public void onCreate(SQLiteDatabase db) { Cursor c=db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='constants'", null); try { if (c.getCount()==0) { db.execSQL("CREATE TABLE constants (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL);"); ContentValues cv=new ContentValues(); cv.put(Constants.TITLE, "Gravity, Death Star I"); cv.put(Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Earth"); cv.put(Constants.VALUE, SensorManager.GRAVITY_EARTH); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Jupiter"); cv.put(Constants.VALUE, SensorManager.GRAVITY_JUPITER); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Mars"); cv.put(Constants.VALUE, SensorManager.GRAVITY_MARS); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Mercury"); cv.put(Constants.VALUE, SensorManager.GRAVITY_MERCURY); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Moon"); cv.put(Constants.VALUE, SensorManager.GRAVITY_MOON); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Neptune"); cv.put(Constants.VALUE, SensorManager.GRAVITY_NEPTUNE); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Pluto"); cv.put(Constants.VALUE, SensorManager.GRAVITY_PLUTO); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Saturn"); cv.put(Constants.VALUE, SensorManager.GRAVITY_SATURN); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Sun"); cv.put(Constants.VALUE, SensorManager.GRAVITY_SUN); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, The Island"); cv.put(Constants.VALUE, SensorManager.GRAVITY_THE_ISLAND); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Uranus"); cv.put(Constants.VALUE, SensorManager.GRAVITY_URANUS); db.insert("constants", getNullColumnHack(), cv); cv.put(Constants.TITLE, "Gravity, Venus"); cv.put(Constants.VALUE, SensorManager.GRAVITY_VENUS); db.insert("constants", getNullColumnHack(), cv); } } finally { c.close(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { android.util.Log.w("Constants", "Upgrading database, which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS constants"); onCreate(db); } } private SQLiteDatabase db; @Override public boolean onCreate() { db=(new DatabaseHelper(getContext())).getWritableDatabase(); return (db == null) ? false : true; } @Override public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) { SQLiteQueryBuilder qb=new SQLiteQueryBuilder(); qb.setTables(getTableName()); if (isCollectionUri(url)) { qb.setProjectionMap(getDefaultProjection()); } else { qb.appendWhere(getIdColumnName()+"="+url.getPathSegments().get(1)); } String orderBy; if (TextUtils.isEmpty(sort)) { orderBy=getDefaultSortOrder(); } else { orderBy=sort; } Cursor c=qb.query(db, projection, selection, selectionArgs, null, null, orderBy); c.setNotificationUri(getContext().getContentResolver(), url); return c; } @Override public String getType(Uri url) { if (isCollectionUri(url)) { return(getCollectionType()); } return(getSingleType()); } @Override public Uri insert(Uri url, ContentValues initialValues) { long rowID; ContentValues values; if (initialValues!=null) { values=new ContentValues(initialValues); } else { values=new ContentValues(); } if (!isCollectionUri(url)) { throw new IllegalArgumentException("Unknown URL " + url); } for (String colName : getRequiredColumns()) { if (values.containsKey(colName) == false) { throw new IllegalArgumentException("Missing column: "+colName); } } populateDefaultValues(values); rowID=db.insert(getTableName(), getNullColumnHack(), values); if (rowID > 0) { Uri uri=ContentUris.withAppendedId(getContentUri(), rowID); getContext().getContentResolver().notifyChange(uri, null); return uri; } throw new SQLException("Failed to insert row into " + url); } @Override public int delete(Uri url, String where, String[] whereArgs) { int count; //long rowId=0; if (isCollectionUri(url)) { count=db.delete(getTableName(), where, whereArgs); } else { String segment=url.getPathSegments().get(1); //rowId=Long.parseLong(segment); count=db.delete(getTableName(), getIdColumnName()+"=" + segment + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); } getContext().getContentResolver().notifyChange(url, null); return count; } @Override public int update(Uri url, ContentValues values, String where, String[] whereArgs) { int count; if (isCollectionUri(url)) { count=db.update(getTableName(), values, where, whereArgs); } else { String segment=url.getPathSegments().get(1); count=db.update(getTableName(), values, getIdColumnName()+"=" + segment+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); } getContext().getContentResolver().notifyChange(url, null); return count; } private boolean isCollectionUri(Uri url) { return(MATCHER.match(url)==CONSTANTS); } private HashMap<String, String> getDefaultProjection() { return(CONSTANTS_LIST_PROJECTION); } private String getTableName() { return("constants"); } private String getIdColumnName() { return("_id"); } private String getDefaultSortOrder() { return("title"); } private String getCollectionType() { return("vnd.android.cursor.dir/vnd.bogotobogo.constant"); } private String getSingleType() { return("vnd.android.cursor.item/vnd.bogotobogo.constant"); } private String[] getRequiredColumns() { return(new String[] {"title"}); } private void populateDefaultValues(ContentValues values) { //Long now=Long.valueOf(System.currentTimeMillis()); //Resources r=Resources.getSystem(); if (values.containsKey(Provider.Constants.VALUE) == false) { values.put(Provider.Constants.VALUE, 0.0f); } } private String getNullColumnHack() { return("title"); } private Uri getContentUri() { return(Provider.Constants.CONTENT_URI); } }
strings.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Content Provider - Planet Constants</string> <string name="ok">OK</string> <string name="cancel">Cancel</string> <string name="add_title">Add Constant</string> <string name="delete_title">Delete Constant: Are You Sure?</string> <string name="display">Display Name:</string> <string name="value">Value:</string> </resources>
res/layout/row.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true"/> <TextView android:id="@+id/value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true"/> </RelativeLayout>
res/layout/add_edit.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:text="@string/display" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:inputType="text" android:id="@+id/title" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:text="@string/value" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:inputType="text" android:id="@+id/value" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
Time to run our example with API level 8 (Android 2.2):
With long click:
Files used in this Content Provider example, ContentProvider_PlanetConstants.zip
If we run with API level 14, Android 4.0, we get the Action Bar for the menu.
This requires slight change in the code, PlanetConstants.java:
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.my_menu, menu); return true; }
and we need to import android.view.MenuInflater. Also, we added res/menu/my_menu.xml:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/add_title" android:icon="@drawable/add" android:showAsAction="ifRoom|withText" android:title="@string/add_title" /> <item android:id="@+id/cancel" android:icon="@drawable/eject" android:showAsAction="ifRoom|withText" android:title="@string/cancel" /> </menu>
with a slightly modified AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bogotobogo.constants"> <uses-sdk android:minSdkVersion="14" /> <application android:label="@string/app_name" android:icon="@drawable/icon"> <provider android:name="com.bogotobogo.constants.Provider" android:authorities="com.bogotobogo.android.constants.Provider" /> <activity android:name="com.bogotobogo.constants.PlanetConstants" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization